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 <noreply@anthropic.com>
This commit is contained in:
Guido Schmit
2026-07-15 18:35:03 +02:00
parent fd804000c5
commit 749eccfaaa
8 changed files with 373 additions and 19 deletions

View File

@@ -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<String>
@@ -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<String>
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"
}
}

View File

@@ -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()
}

View File

@@ -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",

View File

@@ -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<CalendarFilterEntry>,
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<Group>, 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() },
)
}
}
}

View File

@@ -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<CalEvent?>(null) }
var editor by remember { mutableStateOf<EditorRequest?>(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,

View File

@@ -62,6 +62,8 @@ data class CalendarUiState(
val allCalendars: List<CalendarFilterEntry> = 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<String> get() = settingsStore.calendarOrder
fun setCalendarOrder(keys: List<String>) { settingsStore.calendarOrder = keys }
/** Default duration (minutes) for a new event's end time. */
val defaultEventDurationMinutes: Int get() = settingsStore.loadSettings().defaultEventDurationMinutes

View File

@@ -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))
}

View File

@@ -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<String> get() = settingsStore.syncableKeys