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:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user