Android: full-screen menu landing page + much denser settings
- gear now opens a full-screen Menu screen (Profile / Appearance / Accounts / Groups / Sync / Switch server / Logout), replacing the bottom-sheet menu that sat partly under the system nav buttons. Each entry opens its own full-screen page and returns to the menu on close. - settings 'Darstellung' is the appearance table only again (no long scroll to reach navigation) - much more compact settings: dropdowns use a small outlined chip instead of a full-height OutlinedTextField, smaller sync icons, bodyMedium row labels Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -73,13 +73,14 @@ import com.scarriffle.calendarr.ui.event.EventDetailScreen
|
|||||||
import com.scarriffle.calendarr.ui.event.EventEditorSheet
|
import com.scarriffle.calendarr.ui.event.EventEditorSheet
|
||||||
import com.scarriffle.calendarr.ui.groups.GroupIcon
|
import com.scarriffle.calendarr.ui.groups.GroupIcon
|
||||||
import com.scarriffle.calendarr.ui.groups.GroupsScreen
|
import com.scarriffle.calendarr.ui.groups.GroupsScreen
|
||||||
|
import com.scarriffle.calendarr.ui.menu.MenuScreen
|
||||||
import com.scarriffle.calendarr.ui.profile.ProfileScreen
|
import com.scarriffle.calendarr.ui.profile.ProfileScreen
|
||||||
import com.scarriffle.calendarr.ui.settings.SettingsScreen
|
import com.scarriffle.calendarr.ui.settings.SettingsScreen
|
||||||
import com.scarriffle.calendarr.ui.tr
|
import com.scarriffle.calendarr.ui.tr
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
private enum class Overlay { NONE, PROFILE, SETTINGS, ACCOUNTS, GROUPS }
|
private enum class Overlay { NONE, MENU, PROFILE, SETTINGS, ACCOUNTS, GROUPS }
|
||||||
|
|
||||||
data class EditorRequest(val existing: CalEvent?, val date: LocalDate, val prefill: CalEvent? = null)
|
data class EditorRequest(val existing: CalEvent?, val date: LocalDate, val prefill: CalEvent? = null)
|
||||||
|
|
||||||
@@ -198,7 +199,7 @@ fun CalendarScreen(
|
|||||||
CalendarDrawerContent(
|
CalendarDrawerContent(
|
||||||
state = state, vm = vm, username = username, serverUrl = serverUrl,
|
state = state, vm = vm, username = username, serverUrl = serverUrl,
|
||||||
calendars = drawerCalendars,
|
calendars = drawerCalendars,
|
||||||
onOpenMenu = { drawerScope.launch { drawerState.close() }; overlay = Overlay.SETTINGS },
|
onOpenMenu = { drawerScope.launch { drawerState.close() }; overlay = Overlay.MENU },
|
||||||
onSync = { drawerScope.launch { drawerState.close() }; vm.syncWithServer() },
|
onSync = { drawerScope.launch { drawerState.close() }; vm.syncWithServer() },
|
||||||
onClose = { drawerScope.launch { drawerState.close() } },
|
onClose = { drawerScope.launch { drawerState.close() } },
|
||||||
)
|
)
|
||||||
@@ -363,23 +364,30 @@ fun CalendarScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (overlay) {
|
when (overlay) {
|
||||||
Overlay.PROFILE -> ProfileScreen(onClose = { overlay = Overlay.NONE })
|
Overlay.MENU -> MenuScreen(
|
||||||
Overlay.SETTINGS -> SettingsScreen(
|
username = username,
|
||||||
onClose = { overlay = Overlay.NONE; vm.refreshMonthViewMode() },
|
serverUrl = serverUrl,
|
||||||
onSettingsChanged = onSettingsChanged,
|
onClose = { overlay = Overlay.NONE },
|
||||||
onSettingsSynced = onSettingsSynced,
|
onProfile = { overlay = Overlay.PROFILE },
|
||||||
onOpenProfile = { overlay = Overlay.PROFILE },
|
onAppearance = { overlay = Overlay.SETTINGS },
|
||||||
onOpenAccounts = { overlay = Overlay.ACCOUNTS },
|
onAccounts = { overlay = Overlay.ACCOUNTS },
|
||||||
onOpenGroups = { overlay = Overlay.GROUPS },
|
onGroups = { overlay = Overlay.GROUPS },
|
||||||
|
onSync = { overlay = Overlay.NONE; vm.syncWithServer() },
|
||||||
onSwitchServer = onSwitchServer,
|
onSwitchServer = onSwitchServer,
|
||||||
onLogout = onLogout,
|
onLogout = onLogout,
|
||||||
)
|
)
|
||||||
|
Overlay.PROFILE -> ProfileScreen(onClose = { overlay = Overlay.MENU })
|
||||||
|
Overlay.SETTINGS -> SettingsScreen(
|
||||||
|
onClose = { overlay = Overlay.MENU; vm.refreshMonthViewMode() },
|
||||||
|
onSettingsChanged = onSettingsChanged,
|
||||||
|
onSettingsSynced = onSettingsSynced,
|
||||||
|
)
|
||||||
Overlay.ACCOUNTS -> AccountsScreen(
|
Overlay.ACCOUNTS -> AccountsScreen(
|
||||||
onClose = { overlay = Overlay.NONE },
|
onClose = { overlay = Overlay.MENU },
|
||||||
onChanged = { vm.loadWritableCalendars(); vm.syncWithServer() },
|
onChanged = { vm.loadWritableCalendars(); vm.syncWithServer() },
|
||||||
)
|
)
|
||||||
Overlay.GROUPS -> GroupsScreen(
|
Overlay.GROUPS -> GroupsScreen(
|
||||||
onClose = { overlay = Overlay.NONE },
|
onClose = { overlay = Overlay.MENU },
|
||||||
onChanged = { vm.loadGroups(); vm.loadWritableCalendars() },
|
onChanged = { vm.loadGroups(); vm.loadWritableCalendars() },
|
||||||
onOpenGroupView = { g -> overlay = Overlay.NONE; vm.switchGroup(g) },
|
onOpenGroupView = { g -> overlay = Overlay.NONE; vm.switchGroup(g) },
|
||||||
)
|
)
|
||||||
|
|||||||
124
app/src/main/java/com/scarriffle/calendarr/ui/menu/MenuScreen.kt
Normal file
124
app/src/main/java/com/scarriffle/calendarr/ui/menu/MenuScreen.kt
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package com.scarriffle.calendarr.ui.menu
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.WindowInsets
|
||||||
|
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.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Dns
|
||||||
|
import androidx.compose.material.icons.filled.Logout
|
||||||
|
import androidx.compose.material.icons.filled.Palette
|
||||||
|
import androidx.compose.material.icons.filled.People
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.Sync
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.scarriffle.calendarr.ui.tr
|
||||||
|
|
||||||
|
/** Full-screen menu / landing page reached from the drawer's gear: the hub for
|
||||||
|
* Profile / Appearance / Accounts / Groups / Sync / Server / Logout. Each entry
|
||||||
|
* opens its own full-screen page. */
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MenuScreen(
|
||||||
|
username: String,
|
||||||
|
serverUrl: String,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
onProfile: () -> Unit,
|
||||||
|
onAppearance: () -> Unit,
|
||||||
|
onAccounts: () -> Unit,
|
||||||
|
onGroups: () -> Unit,
|
||||||
|
onSync: () -> Unit,
|
||||||
|
onSwitchServer: () -> Unit,
|
||||||
|
onLogout: () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(tr("nav.menu")) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onClose) { Icon(Icons.Filled.Close, contentDescription = tr("common.close")) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets(0),
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()).navigationBarsPadding(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier.size(44.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)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(14.dp))
|
||||||
|
Column {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
MenuRow(Icons.Filled.Person, tr("menu.profile"), onProfile)
|
||||||
|
MenuRow(Icons.Filled.Palette, tr("menu.appearance"), onAppearance)
|
||||||
|
MenuRow(Icons.Filled.CalendarMonth, tr("menu.accounts"), onAccounts)
|
||||||
|
MenuRow(Icons.Filled.People, tr("menu.groups"), onGroups)
|
||||||
|
Divider(Modifier.padding(vertical = 4.dp))
|
||||||
|
MenuRow(Icons.Filled.Sync, tr("menu.sync"), onSync)
|
||||||
|
MenuRow(Icons.Filled.Dns, tr("server.switch"), onSwitchServer)
|
||||||
|
MenuRow(Icons.Filled.Logout, tr("menu.logout"), onLogout, destructive = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MenuRow(icon: ImageVector, label: String, onClick: () -> Unit, destructive: Boolean = false) {
|
||||||
|
val tint = if (destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 20.dp, vertical = 14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(icon, contentDescription = null, tint = tint)
|
||||||
|
Spacer(Modifier.width(18.dp))
|
||||||
|
Text(label, style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = if (destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
package com.scarriffle.calendarr.ui.menu
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
|
||||||
import androidx.compose.material.icons.filled.CalendarMonth
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material.icons.filled.Dns
|
|
||||||
import androidx.compose.material.icons.filled.Logout
|
|
||||||
import androidx.compose.material.icons.filled.Palette
|
|
||||||
import androidx.compose.material.icons.filled.People
|
|
||||||
import androidx.compose.material.icons.filled.Person
|
|
||||||
import androidx.compose.material.icons.filled.Sync
|
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.scarriffle.calendarr.domain.model.Group
|
|
||||||
import com.scarriffle.calendarr.ui.groups.GroupIcon
|
|
||||||
import com.scarriffle.calendarr.ui.tr
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun MenuSheet(
|
|
||||||
isAdmin: Boolean,
|
|
||||||
groups: List<Group>,
|
|
||||||
activeGroup: Group?,
|
|
||||||
onSwitchGroup: (Group?) -> Unit,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onProfile: () -> Unit,
|
|
||||||
onAppearance: () -> Unit,
|
|
||||||
onAccounts: () -> Unit,
|
|
||||||
onGroups: () -> Unit,
|
|
||||||
onSync: () -> Unit,
|
|
||||||
onLogout: () -> Unit,
|
|
||||||
onSwitchServer: () -> Unit,
|
|
||||||
) {
|
|
||||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
|
||||||
// Inset for the system navigation bar so the last rows aren't hidden
|
|
||||||
// behind the Android nav buttons; scroll as a fallback on short screens.
|
|
||||||
Column(
|
|
||||||
Modifier.fillMaxWidth()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.navigationBarsPadding()
|
|
||||||
.padding(bottom = 12.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"Calendarr",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp),
|
|
||||||
)
|
|
||||||
MenuRow(Icons.Filled.AccountCircle, tr("menu.profile"), onProfile)
|
|
||||||
MenuRow(Icons.Filled.Palette, tr("menu.appearance"), onAppearance)
|
|
||||||
MenuRow(Icons.Filled.CalendarMonth, tr("menu.accounts"), onAccounts)
|
|
||||||
// Group view switch (personal + each group) — moved here from the
|
|
||||||
// top bar to free up space. Group *management* stays below.
|
|
||||||
if (groups.isNotEmpty()) {
|
|
||||||
Divider(Modifier.padding(vertical = 4.dp))
|
|
||||||
MenuSectionLabel(tr("groups.view"))
|
|
||||||
GroupSwitchRow(
|
|
||||||
label = tr("group.switch.personal"),
|
|
||||||
checked = activeGroup == null,
|
|
||||||
leading = { Icon(Icons.Filled.Person, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant) },
|
|
||||||
onClick = { onSwitchGroup(null) },
|
|
||||||
)
|
|
||||||
groups.forEach { g ->
|
|
||||||
GroupSwitchRow(
|
|
||||||
label = g.name,
|
|
||||||
checked = activeGroup?.id == g.id,
|
|
||||||
leading = { GroupIcon(g.icon) },
|
|
||||||
onClick = { onSwitchGroup(g) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Divider(Modifier.padding(vertical = 4.dp))
|
|
||||||
MenuRow(Icons.Filled.People, tr("menu.groups"), onGroups)
|
|
||||||
Divider(Modifier.padding(vertical = 4.dp))
|
|
||||||
MenuRow(Icons.Filled.Sync, tr("menu.sync"), onSync)
|
|
||||||
Divider(Modifier.padding(vertical = 4.dp))
|
|
||||||
MenuRow(Icons.Filled.Logout, tr("menu.logout"), onLogout)
|
|
||||||
MenuRow(Icons.Filled.Dns, tr("server.switch"), onSwitchServer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MenuRow(icon: ImageVector, label: String, onClick: () -> Unit) {
|
|
||||||
Row(
|
|
||||||
Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 20.dp, vertical = 14.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
||||||
Spacer(Modifier.width(18.dp))
|
|
||||||
Text(label, style = MaterialTheme.typography.bodyLarge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MenuSectionLabel(text: String) {
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 4.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A group-view choice (personal or a specific group) with a trailing check. */
|
|
||||||
@Composable
|
|
||||||
private fun GroupSwitchRow(
|
|
||||||
label: String,
|
|
||||||
checked: Boolean,
|
|
||||||
leading: @Composable () -> Unit,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 20.dp, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
leading()
|
|
||||||
Spacer(Modifier.width(18.dp))
|
|
||||||
Text(label, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
|
||||||
if (checked) {
|
|
||||||
Icon(Icons.Filled.Check, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,6 +22,9 @@ import androidx.compose.material.icons.filled.Close
|
|||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
@@ -68,11 +71,6 @@ fun SettingsScreen(
|
|||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
onSettingsChanged: (AppSettings) -> Unit,
|
onSettingsChanged: (AppSettings) -> Unit,
|
||||||
onSettingsSynced: () -> Unit,
|
onSettingsSynced: () -> Unit,
|
||||||
onOpenProfile: () -> Unit = {},
|
|
||||||
onOpenAccounts: () -> Unit = {},
|
|
||||||
onOpenGroups: () -> Unit = {},
|
|
||||||
onSwitchServer: () -> Unit = {},
|
|
||||||
onLogout: () -> Unit = {},
|
|
||||||
vm: SettingsViewModel = hiltViewModel(),
|
vm: SettingsViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val initialSettings = LocalAppSettings.current
|
val initialSettings = LocalAppSettings.current
|
||||||
@@ -214,14 +212,6 @@ fun SettingsScreen(
|
|||||||
Switch(checked = hideMenu, onCheckedChange = { hideMenu = it; vm.hideMenuButton = it })
|
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))
|
Text(tr("settings.device.footer"), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 44.dp, top = 6.dp))
|
||||||
|
|
||||||
Divider(Modifier.padding(vertical = 16.dp))
|
|
||||||
Section(tr("settings.more"))
|
|
||||||
NavRow(tr("menu.accounts"), onOpenAccounts)
|
|
||||||
NavRow(tr("groups.title"), onOpenGroups)
|
|
||||||
NavRow(tr("menu.profile"), onOpenProfile)
|
|
||||||
NavRow(tr("menu.server"), onSwitchServer)
|
|
||||||
NavRow(tr("menu.logout"), onLogout, destructive = true)
|
|
||||||
Spacer(Modifier.size(40.dp))
|
Spacer(Modifier.size(40.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,47 +229,39 @@ private fun Section(title: String) {
|
|||||||
Text(title, style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(bottom = 8.dp))
|
Text(title, style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(bottom = 8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NavRow(label: String, onClick: () -> Unit, destructive: Boolean = false) {
|
|
||||||
Row(
|
|
||||||
Modifier.fillMaxWidth().clickable(onClick = onClick).padding(vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = if (destructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Leading per-row sync toggle: highlighted = synced across devices. */
|
/** Leading per-row sync toggle: highlighted = synced across devices. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun SyncIcon(on: Boolean, onClick: () -> Unit) {
|
private fun SyncIcon(on: Boolean, onClick: () -> Unit) {
|
||||||
IconButton(onClick = onClick, modifier = Modifier.size(36.dp)) {
|
IconButton(onClick = onClick, modifier = Modifier.size(32.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Refresh,
|
Icons.Filled.Refresh,
|
||||||
contentDescription = tr("settings.sync_this"),
|
contentDescription = tr("settings.sync_this"),
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
tint = if (on) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = if (on) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
// Compact dropdown trigger (a small outlined chip) — much shorter than a full
|
||||||
|
// OutlinedTextField, so the settings rows aren't inflated.
|
||||||
@Composable
|
@Composable
|
||||||
private fun Dropdown(options: List<Pair<String, String>>, selected: String, onSelect: (String) -> Unit) {
|
private fun Dropdown(options: List<Pair<String, String>>, selected: String, onSelect: (String) -> Unit) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
val selectedLabel = options.firstOrNull { it.first == selected }?.second ?: selected
|
val selectedLabel = options.firstOrNull { it.first == selected }?.second ?: selected
|
||||||
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }, modifier = Modifier.width(170.dp)) {
|
Box {
|
||||||
OutlinedTextField(
|
Row(
|
||||||
value = selectedLabel,
|
Modifier
|
||||||
onValueChange = {},
|
.clip(RoundedCornerShape(8.dp))
|
||||||
readOnly = true,
|
.border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(8.dp))
|
||||||
singleLine = true,
|
.clickable { expanded = true }
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
.padding(start = 12.dp, end = 6.dp, top = 6.dp, bottom = 6.dp),
|
||||||
modifier = Modifier.menuAnchor().fillMaxWidth(),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
)
|
) {
|
||||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
Text(selectedLabel, style = MaterialTheme.typography.bodyMedium, maxLines = 1)
|
||||||
|
Icon(Icons.Filled.ArrowDropDown, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
options.forEach { (key, label) ->
|
options.forEach { (key, label) ->
|
||||||
DropdownMenuItem(text = { Text(label) }, onClick = { onSelect(key); expanded = false })
|
DropdownMenuItem(text = { Text(label) }, onClick = { onSelect(key); expanded = false })
|
||||||
}
|
}
|
||||||
@@ -299,7 +281,7 @@ private fun SyncDropdownRow(
|
|||||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
SyncIcon(synced, onToggleSync)
|
SyncIcon(synced, onToggleSync)
|
||||||
Spacer(Modifier.size(8.dp))
|
Spacer(Modifier.size(8.dp))
|
||||||
Text(label, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
Text(label, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||||
Dropdown(options, selected, onSelect)
|
Dropdown(options, selected, onSelect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,7 +295,7 @@ private fun DeviceDropdownRow(
|
|||||||
) {
|
) {
|
||||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Spacer(Modifier.size(44.dp))
|
Spacer(Modifier.size(44.dp))
|
||||||
Text(label, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
Text(label, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||||
Dropdown(options, selected, onSelect)
|
Dropdown(options, selected, onSelect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,7 +305,7 @@ private fun SyncSwitchRow(label: String, checked: Boolean, onChange: (Boolean) -
|
|||||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
SyncIcon(synced, onToggleSync)
|
SyncIcon(synced, onToggleSync)
|
||||||
Spacer(Modifier.size(8.dp))
|
Spacer(Modifier.size(8.dp))
|
||||||
Text(label, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
Text(label, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||||
Switch(checked = checked, onCheckedChange = onChange)
|
Switch(checked = checked, onCheckedChange = onChange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,7 +323,7 @@ private fun SyncColorRow(
|
|||||||
Row(Modifier.fillMaxWidth().padding(vertical = 6.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.fillMaxWidth().padding(vertical = 6.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
SyncIcon(synced, onToggleSync)
|
SyncIcon(synced, onToggleSync)
|
||||||
Spacer(Modifier.size(8.dp))
|
Spacer(Modifier.size(8.dp))
|
||||||
Text(label, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
Text(label, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||||
Text(colorFromHex(current).toHex(), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(colorFromHex(current).toHex(), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
Spacer(Modifier.size(12.dp))
|
Spacer(Modifier.size(12.dp))
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
Reference in New Issue
Block a user