From 749eccfaaa26e9d698bbf3f10321461588416172 Mon Sep 17 00:00:00 2001 From: Guido Schmit Date: Wed, 15 Jul 2026 18:35:03 +0200 Subject: [PATCH] feat: left navigation drawer (calendars + groups + view + menu) Mirror the iOS side drawer on Android with a ModalNavigationDrawer opened by the hamburger or an edge-swipe (replaces the calendar filter bottom sheet): - header with avatar/name/server + manual sync, full menu (gear), and close - view + group switcher chips - flat calendar list: tap toggles visibility, long-press opens a context menu (hide/banish + reminders), and a Sort toggle reveals up/down reordering. Order is device-local (SettingsStore.calendarOrder, mirrors the web). - month-switching is now a named dropdown (scroll vs pages) like web/iOS - new device-local 'Hide menu button' setting (drawer opens via edge-swipe) Co-Authored-By: Claude Opus 4.8 --- .../calendarr/data/SettingsStore.kt | 19 ++ .../scarriffle/calendarr/ui/CalendarrRoot.kt | 2 + .../java/com/scarriffle/calendarr/ui/L10n.kt | 12 + .../calendarr/ui/calendar/CalendarDrawer.kt | 280 ++++++++++++++++++ .../calendarr/ui/calendar/CalendarScreen.kt | 46 ++- .../ui/calendar/CalendarViewModel.kt | 14 +- .../calendarr/ui/settings/SettingsScreen.kt | 14 +- .../ui/settings/SettingsViewModel.kt | 5 + 8 files changed, 373 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarDrawer.kt diff --git a/app/src/main/java/com/scarriffle/calendarr/data/SettingsStore.kt b/app/src/main/java/com/scarriffle/calendarr/data/SettingsStore.kt index ae4d177..5e57788 100644 --- a/app/src/main/java/com/scarriffle/calendarr/data/SettingsStore.kt +++ b/app/src/main/java/com/scarriffle/calendarr/data/SettingsStore.kt @@ -159,6 +159,11 @@ class SettingsStore @Inject constructor( get() = prefs.getBoolean(K_MONTH_PAGED, false) set(value) = prefs.edit().putBoolean(K_MONTH_PAGED, value).apply() + /** Device-local: hide the top-bar menu button (drawer opens via edge-swipe). */ + var hideMenuButton: Boolean + get() = prefs.getBoolean(K_HIDE_MENU, false) + set(value) = prefs.edit().putBoolean(K_HIDE_MENU, value).apply() + // --- Hidden calendars ("source:id") --- var hiddenCalendarKeys: Set @@ -180,6 +185,18 @@ class SettingsStore @Inject constructor( get() = prefs.getStringSet(K_REMINDER_DISABLED, emptySet())?.toSet() ?: emptySet() set(value) = prefs.edit().putStringSet(K_REMINDER_DISABLED, value).apply() + // --- Calendar display order ("source:id" keys), device-local (mirrors web cal_order) --- + + var calendarOrder: List + get() { + val raw = prefs.getString(K_CAL_ORDER, null) ?: return emptyList() + return runCatching { + val arr = org.json.JSONArray(raw) + (0 until arr.length()).map { arr.getString(it) } + }.getOrDefault(emptyList()) + } + set(value) = prefs.edit().putString(K_CAL_ORDER, org.json.JSONArray(value).toString()).apply() + private companion object { const val K_DEFAULT_VIEW = "default_view" const val K_WEEK_START = "week_start_day" @@ -201,8 +218,10 @@ class SettingsStore @Inject constructor( const val K_DEFAULT_DURATION = "default_event_duration_minutes" const val K_CACHE_MONTHS = "cache_months" const val K_MONTH_PAGED = "month_view_paged" + const val K_HIDE_MENU = "hide_menu_button" const val K_HIDDEN = "hidden_calendar_keys" const val K_BANISHED = "banished_calendar_keys" const val K_REMINDER_DISABLED = "reminder_disabled_calendar_keys" + const val K_CAL_ORDER = "calendar_order" } } 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 4ef00b3..b2f4f3f 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/CalendarrRoot.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/CalendarrRoot.kt @@ -69,6 +69,8 @@ fun CalendarrRoot(vm: MainViewModel = hiltViewModel()) { onSwitchServer = vm::switchServer, onSettingsChanged = vm::applyLocalSettings, onSettingsSynced = vm::refreshSettings, + username = vm.username, + serverUrl = vm.serverUrl, ) } ?: SplashScreen() } diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt b/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt index aa750af..8ad10b0 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt @@ -63,6 +63,10 @@ object L10n { "settings.reset" to "Zurücksetzen", "settings.appearance" to "Ansicht", "settings.device" to "Nur auf diesem Gerät", + "settings.hide_menu_button" to "Menü-Button ausblenden", + "settings.month_mode" to "Monatswechsel", + "settings.month_mode.scroll" to "Fortlaufend scrollen", + "settings.month_mode.paged" to "Seitenweise blättern", "settings.device.footer" to "Diese Einstellungen gelten nur auf diesem Gerät und werden nicht synchronisiert.", "settings.directory_hidden" to "Profil verbergen", "settings.directory_hidden.desc" to "Nicht in der Teilen-/Gruppen-Auswahl anderer Nutzer erscheinen.", @@ -136,6 +140,8 @@ object L10n { "accounts.ha.add" to "Home Assistant hinzufügen", "filter.title" to "Kalender", "filter.empty" to "Keine Kalender vorhanden", "filter.show_all" to "Alle anzeigen", "filter.hide_all" to "Alle ausblenden", + "filter.sort" to "Sortieren", "filter.done" to "Fertig", + "filter.move_up" to "Nach oben", "filter.move_down" to "Nach unten", "filter.button" to "Kalender ein-/ausblenden", "filter.sync_error" to "Synchronisierung fehlgeschlagen", "filter.banish" to "Dauerhaft ausblenden", @@ -236,6 +242,10 @@ object L10n { "settings.reset" to "Reset", "settings.appearance" to "View", "settings.device" to "This device only", + "settings.hide_menu_button" to "Hide menu button", + "settings.month_mode" to "Month switching", + "settings.month_mode.scroll" to "Continuous scroll", + "settings.month_mode.paged" to "Page by page", "settings.device.footer" to "These settings apply to this device only and are not synced.", "settings.directory_hidden" to "Hide profile", "settings.directory_hidden.desc" to "Don't appear in other users' share/group pickers.", @@ -309,6 +319,8 @@ object L10n { "accounts.ha.add" to "Add Home Assistant", "filter.title" to "Calendars", "filter.empty" to "No calendars available", "filter.show_all" to "Show all", "filter.hide_all" to "Hide all", + "filter.sort" to "Sort", "filter.done" to "Done", + "filter.move_up" to "Move up", "filter.move_down" to "Move down", "filter.button" to "Show/hide calendars", "filter.sync_error" to "Sync failed", "filter.banish" to "Hide permanently", diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarDrawer.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarDrawer.kt new file mode 100644 index 0000000..c064734 --- /dev/null +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarDrawer.kt @@ -0,0 +1,280 @@ +package com.scarriffle.calendarr.ui.calendar + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.horizontalScroll +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.fillMaxHeight +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.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Archive +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.NotificationsOff +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.Switch +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.scarriffle.calendarr.domain.model.CalViewType +import com.scarriffle.calendarr.domain.model.Group +import com.scarriffle.calendarr.ui.groups.GroupIcon +import com.scarriffle.calendarr.ui.tr +import com.scarriffle.calendarr.util.colorFromHex + +/** + * The left navigation drawer: calendar visibility (flat, reorderable) + view / + * group switching + a header with manual sync, the full menu, and close. Mirrors + * the iOS side drawer. Order is device-local (CalendarViewModel.calendarOrder). + */ +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun CalendarDrawerContent( + state: CalendarUiState, + vm: CalendarViewModel, + username: String, + serverUrl: String, + calendars: List, + onOpenMenu: () -> Unit, + onSync: () -> Unit, + onClose: () -> Unit, +) { + var isSorting by remember { mutableStateOf(false) } + + // Working order: entries sorted by the stored order (unknown → end, by name). + var order by remember(calendars, vm.calendarOrder) { + mutableStateOf( + calendars.sortedWith( + compareBy( + { vm.calendarOrder.indexOf(it.key).let { i -> if (i < 0) Int.MAX_VALUE else i } }, + { it.name.lowercase() }, + ) + ).map { it.key } + ) + } + val entries = order.mapNotNull { key -> calendars.firstOrNull { it.key == key } } + + fun move(index: Int, delta: Int) { + val to = index + delta + if (to < 0 || to >= order.size) return + val m = order.toMutableList() + val x = m.removeAt(index); m.add(to, x) + order = m + vm.setCalendarOrder(m) + } + + ModalDrawerSheet(Modifier.width(320.dp).fillMaxHeight()) { + Column(Modifier.fillMaxHeight()) { + header(username, serverUrl, onSync, onOpenMenu, onClose) + Divider() + viewSwitcher(state.viewType) { vm.setViewType(it) } + Divider() + if (state.groups.isNotEmpty()) { + groupSwitcher(state.groups, state.activeGroup) { vm.switchGroup(it) } + Divider() + } + Row( + Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = 8.dp, bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(tr("filter.title"), style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant) + Spacer(Modifier.weight(1f)) + TextButton(onClick = { isSorting = !isSorting }) { + Text(tr(if (isSorting) "filter.done" else "filter.sort")) + } + } + LazyColumn(Modifier.fillMaxWidth().weight(1f).navigationBarsPadding()) { + items(entries, key = { it.key }) { entry -> + CalendarRow( + entry = entry, + visible = entry.key !in state.hiddenKeys, + reminderDisabled = entry.key in state.reminderDisabledKeys, + sorting = isSorting, + onToggle = { vm.setCalendarHidden(entry.key, !it) }, + onBanish = { vm.setCalendarBanished(entry.key, banished = true) }, + onToggleReminders = { vm.setCalendarRemindersDisabled(entry.key, disabled = it) }, + onMoveUp = { move(order.indexOf(entry.key), -1) }, + onMoveDown = { move(order.indexOf(entry.key), 1) }, + ) + } + } + } + } +} + +@Composable +private fun header( + username: String, + serverUrl: String, + onSync: () -> Unit, + onOpenMenu: () -> Unit, + onClose: () -> Unit, +) { + Row( + Modifier.fillMaxWidth().statusBarsPadding().padding(start = 18.dp, end = 8.dp, top = 12.dp, bottom = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + Modifier.size(48.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.Center, + ) { + Text(username.firstOrNull()?.uppercase() ?: "?", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onPrimary, fontWeight = FontWeight.Bold) + } + Column(Modifier.weight(1f).padding(start = 14.dp)) { + Text(username, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, maxLines = 1) + Text(serverUrl.removePrefix("https://").removePrefix("http://"), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1) + } + IconButton(onClick = onSync) { + Icon(Icons.Filled.Refresh, contentDescription = tr("menu.sync"), tint = MaterialTheme.colorScheme.primary) + } + IconButton(onClick = onOpenMenu) { + Icon(Icons.Filled.Settings, contentDescription = tr("menu.appearance"), tint = MaterialTheme.colorScheme.primary) + } + IconButton(onClick = onClose) { + Icon(Icons.Filled.Close, contentDescription = tr("common.close"), tint = MaterialTheme.colorScheme.onSurfaceVariant) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun viewSwitcher(current: CalViewType, onSelect: (CalViewType) -> Unit) { + Row( + Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(horizontal = 12.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + CalViewType.entries.forEach { type -> + FilterChip( + selected = current == type, + onClick = { onSelect(type) }, + label = { Text(tr("view.${type.key}")) }, + leadingIcon = { Icon(type.icon, contentDescription = null, modifier = Modifier.size(18.dp)) }, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun groupSwitcher(groups: List, active: Group?, onSwitch: (Group?) -> Unit) { + Row( + Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(horizontal = 12.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + FilterChip( + selected = active == null, + onClick = { onSwitch(null) }, + label = { Text(tr("group.switch.personal")) }, + ) + groups.forEach { g -> + FilterChip( + selected = active?.id == g.id, + onClick = { onSwitch(g) }, + label = { Text(g.name) }, + leadingIcon = { GroupIcon(g.icon) }, + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun CalendarRow( + entry: CalendarFilterEntry, + visible: Boolean, + reminderDisabled: Boolean, + sorting: Boolean, + onToggle: (Boolean) -> Unit, + onBanish: () -> Unit, + onToggleReminders: (Boolean) -> Unit, + onMoveUp: () -> Unit, + onMoveDown: () -> Unit, +) { + var menuOpen by remember { mutableStateOf(false) } + Box { + Row( + Modifier.fillMaxWidth() + .combinedClickable( + onClick = { if (!sorting) onToggle(visible) }, + onLongClick = { if (!sorting) menuOpen = true }, + ) + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(Modifier.size(14.dp).clip(CircleShape).background(colorFromHex(entry.color))) + Text( + entry.name, + modifier = Modifier.weight(1f).padding(start = 12.dp), + style = MaterialTheme.typography.bodyLarge, + color = if (visible) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, + ) + if (entry.readOnly) { + Icon(Icons.Filled.Lock, contentDescription = tr("filter.read_only"), + tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(end = 6.dp).size(15.dp)) + } + if (sorting) { + IconButton(onClick = onMoveUp) { Icon(Icons.Filled.KeyboardArrowUp, contentDescription = tr("filter.move_up")) } + IconButton(onClick = onMoveDown) { Icon(Icons.Filled.KeyboardArrowDown, contentDescription = tr("filter.move_down")) } + } else { + if (reminderDisabled) { + Icon(Icons.Filled.NotificationsOff, contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(end = 6.dp).size(16.dp)) + } + Switch(checked = visible, onCheckedChange = onToggle) + } + } + DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { + DropdownMenuItem( + text = { Text(tr(if (reminderDisabled) "filter.reminders_on" else "filter.reminders_off")) }, + leadingIcon = { Icon(if (reminderDisabled) Icons.Filled.Notifications else Icons.Filled.NotificationsOff, contentDescription = null) }, + onClick = { menuOpen = false; onToggleReminders(!reminderDisabled) }, + ) + DropdownMenuItem( + text = { Text(tr("filter.banish")) }, + leadingIcon = { Icon(Icons.Filled.Archive, contentDescription = null) }, + onClick = { menuOpen = false; onBanish() }, + ) + } + } +} 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 c1128d5..cd419d4 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 @@ -25,15 +25,17 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ChevronLeft import androidx.compose.material.icons.filled.ChevronRight -import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Today import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Divider import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator @@ -89,6 +91,8 @@ fun CalendarScreen( onSwitchServer: () -> Unit, onSettingsChanged: (AppSettings) -> Unit, onSettingsSynced: () -> Unit, + username: String = "", + serverUrl: String = "", vm: CalendarViewModel = hiltViewModel(), ) { val state by vm.state.collectAsState() @@ -128,7 +132,6 @@ fun CalendarScreen( var viewMenuOpen by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) } - var showFilter by remember { mutableStateOf(false) } var detailEvent by remember { mutableStateOf(null) } var editor by remember { mutableStateOf(null) } var overlay by remember { mutableStateOf(Overlay.NONE) } @@ -139,6 +142,14 @@ fun CalendarScreen( val birthdayScope = rememberCoroutineScope() val birthdayCalName = tr("birthday.calendar_name") + // Side navigation drawer (calendars + groups + view + menu). + val drawerState = rememberDrawerState(DrawerValue.Closed) + val drawerScope = rememberCoroutineScope() + val drawerCalendars = remember(state.events, state.banishedKeys, state.allCalendars) { allKnownCalendars(vm) } + androidx.compose.runtime.LaunchedEffect(drawerState.currentValue) { + if (drawerState.isOpen) vm.loadAllCalendars() + } + // Continuous month scrolling val monthListState = rememberLazyListState() var todaySignal by remember { mutableIntStateOf(0) } @@ -159,6 +170,18 @@ fun CalendarScreen( if (isMonth) todaySignal++ else vm.moveToToday() } + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + CalendarDrawerContent( + state = state, vm = vm, username = username, serverUrl = serverUrl, + calendars = drawerCalendars, + onOpenMenu = { drawerScope.launch { drawerState.close() }; showMenu = true }, + onSync = { drawerScope.launch { drawerState.close() }; vm.syncWithServer() }, + onClose = { drawerScope.launch { drawerState.close() } }, + ) + }, + ) { Scaffold( topBar = { CompactTopBar( @@ -166,11 +189,11 @@ fun CalendarScreen( viewType = state.viewType, loading = state.isLoading || state.isBackgroundCaching, viewMenuOpen = viewMenuOpen, - onMenu = { showMenu = true }, + showMenuButton = !state.hideMenuButton, + onMenu = { drawerScope.launch { drawerState.open() } }, onPrev = { goPrev() }, onToday = { goToday() }, onNext = { goNext() }, - onFilter = { showFilter = true }, onViewMenuToggle = { viewMenuOpen = it }, onSelectView = { vm.setViewType(it) }, ) @@ -242,6 +265,7 @@ fun CalendarScreen( } } } + } // ---- Sheets ---- @@ -262,15 +286,6 @@ fun CalendarScreen( ) } - if (showFilter) { - androidx.compose.runtime.LaunchedEffect(Unit) { vm.loadAllCalendars() } - CalendarFilterSheet( - events = remember(state.events, state.banishedKeys, state.allCalendars) { allKnownCalendars(vm) }, - vm = vm, - onDismiss = { showFilter = false }, - ) - } - dayPreview?.let { date -> DayPreviewDialog( date = date, @@ -450,11 +465,11 @@ private fun CompactTopBar( viewType: CalViewType, loading: Boolean, viewMenuOpen: Boolean, + showMenuButton: Boolean, onMenu: () -> Unit, onPrev: () -> Unit, onToday: () -> Unit, onNext: () -> Unit, - onFilter: () -> Unit, onViewMenuToggle: (Boolean) -> Unit, onSelectView: (CalViewType) -> Unit, ) { @@ -487,7 +502,6 @@ private fun CompactTopBar( strokeWidth = 2.dp, ) } - CompactIcon(Icons.Filled.FilterList, onFilter, tr("filter.button")) Box { CompactIcon(viewType.icon, { onViewMenuToggle(true) }, tr("view.change")) DropdownMenu(expanded = viewMenuOpen, onDismissRequest = { onViewMenuToggle(false) }) { @@ -500,7 +514,7 @@ private fun CompactTopBar( } } } - CompactIcon(Icons.Filled.Menu, onMenu, tr("nav.menu")) + if (showMenuButton) CompactIcon(Icons.Filled.Menu, onMenu, tr("nav.menu")) } Divider( thickness = 0.5.dp, 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 bd837a9..a455928 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 @@ -62,6 +62,8 @@ data class CalendarUiState( val allCalendars: List = emptyList(), // Device-local: month view as horizontal paged (swipe) vs. scroll feed. val monthViewPaged: Boolean = false, + // Device-local: hide the top-bar menu button (drawer opens via edge-swipe). + val hideMenuButton: Boolean = false, ) fun groupMemberKey(ownerId: Int): String = "gm:$ownerId" @@ -127,14 +129,22 @@ class CalendarViewModel @Inject constructor( banishedKeys = settingsStore.banishedCalendarKeys, reminderDisabledKeys = settingsStore.reminderDisabledCalendarKeys, monthViewPaged = settingsStore.monthViewPaged, + hideMenuButton = settingsStore.hideMenuButton, ) } - /** Re-read the device-local month-view mode (called when settings close). */ + /** Re-read the device-local view prefs (called when settings close). */ fun refreshMonthViewMode() { - _state.update { it.copy(monthViewPaged = settingsStore.monthViewPaged) } + _state.update { it.copy( + monthViewPaged = settingsStore.monthViewPaged, + hideMenuButton = settingsStore.hideMenuButton, + ) } } + // Device-local calendar order ("source:id"), mirrors the web cal_order. + val calendarOrder: List get() = settingsStore.calendarOrder + fun setCalendarOrder(keys: List) { settingsStore.calendarOrder = keys } + /** Default duration (minutes) for a new event's end time. */ val defaultEventDurationMinutes: Int get() = settingsStore.loadSettings().defaultEventDurationMinutes diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsScreen.kt index 22c6dac..83e4390 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsScreen.kt @@ -147,7 +147,13 @@ fun SettingsScreen( synced("week_start_day"), { toggle("week_start_day") }, ) SyncSwitchRow(tr("settings.dimpast"), settings.dimPastEvents, { update(settings.copy(dimPastEvents = it)) }, synced("dim_past_events"), { toggle("dim_past_events") }) - SyncSwitchRow(tr("settings.month_paged"), settings.monthViewPaged, { update(settings.copy(monthViewPaged = it)) }, synced("month_view_paged"), { toggle("month_view_paged") }) + SyncDropdownRow( + tr("settings.month_mode"), + listOf("false" to tr("settings.month_mode.scroll"), "true" to tr("settings.month_mode.paged")), + settings.monthViewPaged.toString(), + { update(settings.copy(monthViewPaged = it.toBoolean())) }, + synced("month_view_paged"), { toggle("month_view_paged") }, + ) SyncDropdownRow( tr("settings.hourheight"), listOf("28" to tr("settings.hourheight.compact"), "44" to tr("settings.hourheight.normal"), "60" to tr("settings.hourheight.comfort"), "80" to tr("settings.hourheight.large")), @@ -196,6 +202,12 @@ fun SettingsScreen( listOf("1" to tr("settings.linecontrast.barely"), "2" to tr("settings.linecontrast.subtle"), "3" to tr("settings.linecontrast.normal"), "4" to tr("settings.linecontrast.strong")), settings.lineContrast.toString(), ) { update(settings.copy(lineContrast = it.toInt())) } + var hideMenu by remember { mutableStateOf(vm.hideMenuButton) } + Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) { + Spacer(Modifier.size(44.dp)) + Text(tr("settings.hide_menu_button"), style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) + Switch(checked = hideMenu, onCheckedChange = { hideMenu = it; vm.hideMenuButton = it }) + } Text(tr("settings.device.footer"), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 44.dp, top = 6.dp)) Spacer(Modifier.size(40.dp)) } diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsViewModel.kt index 6f01ef6..f81594e 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsViewModel.kt @@ -28,6 +28,11 @@ class SettingsViewModel @Inject constructor( } } + /** Device-local: hide the top-bar menu button (drawer opens via edge-swipe). */ + var hideMenuButton: Boolean + get() = settingsStore.hideMenuButton + set(value) { settingsStore.hideMenuButton = value } + // ---- Per-setting sync flags ---- val syncableKeys: List get() = settingsStore.syncableKeys