feat: Android Profil-Kapitel in Einstellungen + Kalenderfarb-Endpunkte
Einstellungen zeigen jetzt ein Profil-Kapitel: Anzeigename (editierbar), Anmeldename (read-only), E-Mail + Speichern; Privatsphäre (private Termine beschäftigt/verbergen) und geteilter Kalender (Dropdown der eigenen lokalen Kalender) — serverseitig geladen und gezielt gespeichert. Datenebene um Farb-Endpunkte ergänzt (lokal/iCal PUT + setCalendarColor für caldav/google/ha) als Basis für die Farbbearbeitung. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -210,6 +210,27 @@ class CalendarRepository @Inject constructor(
|
||||
suspend fun deleteHomeAssistantAccount(id: Int) =
|
||||
guarded { api.deleteHomeAssistantAccount(id).ensureSuccess() }
|
||||
|
||||
/** Change a local calendar's colour. */
|
||||
suspend fun updateLocalCalendarColor(id: Int, color: String) = guarded {
|
||||
api.updateLocalCalendar(id, jsonBody("color" to color)).ensureSuccess()
|
||||
}
|
||||
|
||||
/** Change an iCal subscription's colour. */
|
||||
suspend fun updateICalColor(id: Int, color: String) = guarded {
|
||||
api.updateICalSubscription(id, jsonBody("color" to color)).ensureSuccess()
|
||||
}
|
||||
|
||||
/** Set a per-calendar colour for server-managed sources (caldav/google/homeassistant). */
|
||||
suspend fun setCalendarColor(source: String, calendarId: Int, color: String) = guarded {
|
||||
val body = jsonBody("color" to color)
|
||||
when (source) {
|
||||
"caldav" -> api.updateCalDAVCalendar(calendarId, body).ensureSuccess()
|
||||
"google" -> api.updateGoogleCalendar(calendarId, body).ensureSuccess()
|
||||
"homeassistant" -> api.updateHACalendar(calendarId, body).ensureSuccess()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggle a calendar's server-side visibility (caldav/google/homeassistant only). */
|
||||
suspend fun setCalendarSidebarHidden(source: String, calendarId: Int, hidden: Boolean) = guarded {
|
||||
val body = jsonBody("enabled" to !hidden, "sidebar_hidden" to hidden)
|
||||
|
||||
@@ -152,6 +152,9 @@ interface CalendarrApi {
|
||||
@POST("api/local/calendars")
|
||||
suspend fun addLocalCalendar(@Body body: RequestBody): LocalCalendar
|
||||
|
||||
@PUT("api/local/calendars/{id}")
|
||||
suspend fun updateLocalCalendar(@Path("id") id: Int, @Body body: RequestBody): Response<ResponseBody>
|
||||
|
||||
@DELETE("api/local/calendars/{id}")
|
||||
suspend fun deleteLocalCalendar(@Path("id") id: Int): Response<ResponseBody>
|
||||
|
||||
@@ -163,6 +166,9 @@ interface CalendarrApi {
|
||||
@POST("api/ical/subscriptions")
|
||||
suspend fun addICalSubscription(@Body body: RequestBody): ICalSubscription
|
||||
|
||||
@PUT("api/ical/subscriptions/{id}")
|
||||
suspend fun updateICalSubscription(@Path("id") id: Int, @Body body: RequestBody): Response<ResponseBody>
|
||||
|
||||
@DELETE("api/ical/subscriptions/{id}")
|
||||
suspend fun deleteICalSubscription(@Path("id") id: Int): Response<ResponseBody>
|
||||
|
||||
|
||||
@@ -122,6 +122,32 @@ object L10n {
|
||||
"ical.refresh.1h" to "Stündlich", "ical.refresh.6h" to "Alle 6 Std.", "ical.refresh.1d" to "Täglich",
|
||||
"ha.display_name" to "Anzeigename", "ha.url_placeholder" to "URL (z.B. http://homeassistant.local:8123)",
|
||||
"ha.token" to "Long-Lived Access Token", "ha.connect" to "Verbinden",
|
||||
// Collaboration: profile / privacy / sharing / groups / import-export
|
||||
"settings.nav.profile" to "Profil",
|
||||
"profile.display_name" to "Anzeigename", "profile.login_name" to "Anmeldename",
|
||||
"settings.privacy" to "Privatsphäre", "settings.private_visibility" to "Private Termine",
|
||||
"settings.private.busy" to "Als beschäftigt zeigen", "settings.private.hidden" to "Verbergen",
|
||||
"settings.private_visibility.desc" to "Legt fest, wie deine privaten Termine anderen Gruppenmitgliedern erscheinen.",
|
||||
"settings.group_visible" to "Geteilter Kalender",
|
||||
"settings.group_visible.desc" to "Dieser Kalender wird in der Gruppenansicht für andere sichtbar.",
|
||||
"group.visible.none" to "Keiner",
|
||||
"share.title" to "Teilen", "share.with" to "Geteilt mit", "share.add" to "Person hinzufügen",
|
||||
"share.permission.read" to "Lesen", "share.permission.read_write" to "Bearbeiten",
|
||||
"share.none" to "Noch nicht geteilt", "share.search" to "Benutzer suchen",
|
||||
"accounts.shared.header" to "Kalender von anderen", "accounts.shared_by" to "geteilt von %@",
|
||||
"accounts.color" to "Farbe ändern", "accounts.action.share" to "Teilen",
|
||||
"accounts.action.import" to "Importieren", "accounts.action.export" to "Exportieren",
|
||||
"import.title" to "iCal importieren", "import.result" to "%d importiert, %d übersprungen",
|
||||
"import.failed" to "Import fehlgeschlagen", "export.failed" to "Export fehlgeschlagen",
|
||||
"menu.groups" to "Gruppen", "groups.title" to "Gruppen", "groups.empty" to "Noch keine Gruppen",
|
||||
"groups.create" to "Gruppe erstellen", "groups.name" to "Gruppenname",
|
||||
"groups.members" to "Mitglieder", "groups.member_count" to "%d Mitglieder",
|
||||
"groups.icon" to "Symbol", "groups.manage" to "Verwalten",
|
||||
"groups.delete" to "Gruppe löschen", "groups.delete_confirm" to "Diese Gruppe löschen?",
|
||||
"groups.leave" to "Gruppe verlassen", "groups.member_color" to "Farbe",
|
||||
"groups.add_member" to "Mitglied hinzufügen", "groups.view" to "Gruppenansicht",
|
||||
"groups.calendar" to "Gruppenkalender", "group.event" to "Gruppentermin",
|
||||
"group.switch.personal" to "Mein Kalender",
|
||||
// Auth screens
|
||||
"auth.server_title" to "Server verbinden", "auth.server_url" to "Server-URL",
|
||||
"auth.server_hint" to "Gib die Adresse deines Calendarr-Servers ein.",
|
||||
@@ -223,6 +249,32 @@ object L10n {
|
||||
"ical.refresh.1h" to "Hourly", "ical.refresh.6h" to "Every 6 hours", "ical.refresh.1d" to "Daily",
|
||||
"ha.display_name" to "Display name", "ha.url_placeholder" to "URL (e.g. http://homeassistant.local:8123)",
|
||||
"ha.token" to "Long-Lived Access Token", "ha.connect" to "Connect",
|
||||
// Collaboration: profile / privacy / sharing / groups / import-export
|
||||
"settings.nav.profile" to "Profile",
|
||||
"profile.display_name" to "Display name", "profile.login_name" to "Login name",
|
||||
"settings.privacy" to "Privacy", "settings.private_visibility" to "Private events",
|
||||
"settings.private.busy" to "Show as busy", "settings.private.hidden" to "Hidden",
|
||||
"settings.private_visibility.desc" to "Controls how your private events appear to other group members.",
|
||||
"settings.group_visible" to "Shared calendar",
|
||||
"settings.group_visible.desc" to "This calendar is shown to others in the group view.",
|
||||
"group.visible.none" to "None",
|
||||
"share.title" to "Share", "share.with" to "Shared with", "share.add" to "Add person",
|
||||
"share.permission.read" to "Read", "share.permission.read_write" to "Read & write",
|
||||
"share.none" to "Not shared yet", "share.search" to "Search users",
|
||||
"accounts.shared.header" to "Calendars from others", "accounts.shared_by" to "shared by %@",
|
||||
"accounts.color" to "Change color", "accounts.action.share" to "Share",
|
||||
"accounts.action.import" to "Import", "accounts.action.export" to "Export",
|
||||
"import.title" to "Import iCal", "import.result" to "%d imported, %d skipped",
|
||||
"import.failed" to "Import failed", "export.failed" to "Export failed",
|
||||
"menu.groups" to "Groups", "groups.title" to "Groups", "groups.empty" to "No groups yet",
|
||||
"groups.create" to "Create group", "groups.name" to "Group name",
|
||||
"groups.members" to "Members", "groups.member_count" to "%d members",
|
||||
"groups.icon" to "Icon", "groups.manage" to "Manage",
|
||||
"groups.delete" to "Delete group", "groups.delete_confirm" to "Delete this group?",
|
||||
"groups.leave" to "Leave group", "groups.member_color" to "Color",
|
||||
"groups.add_member" to "Add member", "groups.view" to "Group view",
|
||||
"groups.calendar" to "Group calendar", "group.event" to "Group event",
|
||||
"group.switch.personal" to "My calendar",
|
||||
// Auth screens
|
||||
"auth.server_title" to "Connect server", "auth.server_url" to "Server URL",
|
||||
"auth.server_hint" to "Enter the address of your Calendarr server.",
|
||||
|
||||
@@ -21,12 +21,17 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
@@ -86,6 +91,9 @@ fun SettingsScreen(
|
||||
) { padding ->
|
||||
Column(Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||
|
||||
ProfileChapter(vm)
|
||||
Divider(Modifier.padding(vertical = 16.dp))
|
||||
|
||||
Section(tr("settings.calview"))
|
||||
ChipRow(
|
||||
options = CalViewType.entries.map { it.key to tr("view.${it.key}") },
|
||||
@@ -175,6 +183,80 @@ private fun Section(title: String) {
|
||||
Text(title, style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(bottom = 8.dp))
|
||||
}
|
||||
|
||||
/** Server-backed "Profil" chapter: display name, login name, email, privacy, shared calendar. */
|
||||
@Composable
|
||||
private fun ProfileChapter(vm: SettingsViewModel) {
|
||||
val savedLabel = tr("settings.saved")
|
||||
|
||||
Section(tr("settings.nav.profile"))
|
||||
OutlinedTextField(
|
||||
value = vm.displayName,
|
||||
onValueChange = vm::onDisplayNameChange,
|
||||
label = { Text(tr("profile.display_name")) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
|
||||
Text(tr("profile.login_name"), modifier = Modifier.weight(1f), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(vm.loginName, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
Spacer(Modifier.size(8.dp))
|
||||
OutlinedTextField(
|
||||
value = vm.email,
|
||||
onValueChange = vm::onEmailChange,
|
||||
label = { Text(tr("profile.email")) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Button(onClick = { vm.saveProfile(savedLabel) }) { Text(tr("event.save")) }
|
||||
vm.profileMessage?.let {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(it, color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Divider(Modifier.padding(vertical = 16.dp))
|
||||
|
||||
Section(tr("settings.privacy"))
|
||||
ChipRow(
|
||||
options = listOf("busy" to tr("settings.private.busy"), "hidden" to tr("settings.private.hidden")),
|
||||
selected = vm.privateVisibility,
|
||||
onSelect = vm::changePrivateVisibility,
|
||||
)
|
||||
Spacer(Modifier.size(6.dp))
|
||||
Text(tr("settings.private_visibility.desc"), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Divider(Modifier.padding(vertical = 16.dp))
|
||||
|
||||
Section(tr("settings.group_visible"))
|
||||
CalendarDropdown(vm)
|
||||
Spacer(Modifier.size(6.dp))
|
||||
Text(tr("settings.group_visible.desc"), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CalendarDropdown(vm: SettingsViewModel) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val noneLabel = tr("group.visible.none")
|
||||
val selectedLabel = vm.ownLocalCalendars.firstOrNull { it.id == vm.groupVisibleId }?.name ?: noneLabel
|
||||
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
|
||||
OutlinedTextField(
|
||||
value = selectedLabel,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(tr("settings.group_visible")) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier.menuAnchor().fillMaxWidth(),
|
||||
)
|
||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DropdownMenuItem(text = { Text(noneLabel) }, onClick = { vm.changeGroupVisible(0); expanded = false })
|
||||
vm.ownLocalCalendars.forEach { cal ->
|
||||
DropdownMenuItem(text = { Text(cal.name) }, onClick = { vm.changeGroupVisible(cal.id); expanded = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ChipRow(options: List<Pair<String, String>>, selected: String, onSelect: (String) -> Unit) {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.scarriffle.calendarr.ui.settings
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.scarriffle.calendarr.data.CalendarRepository
|
||||
import com.scarriffle.calendarr.data.SettingsStore
|
||||
import com.scarriffle.calendarr.domain.model.AppSettings
|
||||
import com.scarriffle.calendarr.domain.model.LocalCalendar
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -27,4 +31,65 @@ class SettingsViewModel @Inject constructor(
|
||||
var cacheMonths: Int
|
||||
get() = settingsStore.cacheMonths
|
||||
set(value) { settingsStore.cacheMonths = value }
|
||||
|
||||
// ---- Profile chapter (server-backed) ----
|
||||
|
||||
var displayName by mutableStateOf("")
|
||||
var loginName by mutableStateOf("")
|
||||
var email by mutableStateOf("")
|
||||
private set
|
||||
var privateVisibility by mutableStateOf("busy")
|
||||
private set
|
||||
var groupVisibleId by mutableStateOf(0) // 0 = none
|
||||
private set
|
||||
var ownLocalCalendars by mutableStateOf<List<LocalCalendar>>(emptyList())
|
||||
private set
|
||||
var profileMessage by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
init { loadProfile() }
|
||||
|
||||
fun onDisplayNameChange(v: String) { displayName = v }
|
||||
fun onEmailChange(v: String) { email = v }
|
||||
|
||||
private fun loadProfile() {
|
||||
viewModelScope.launch {
|
||||
runCatching { repository.getProfile() }.onSuccess { p ->
|
||||
displayName = p.displayName ?: p.username
|
||||
loginName = p.username
|
||||
email = p.email ?: ""
|
||||
}
|
||||
runCatching { repository.getSettings() }.onSuccess { s ->
|
||||
privateVisibility = s.privateEventVisibility
|
||||
groupVisibleId = s.groupVisibleCalendarId ?: 0
|
||||
}
|
||||
runCatching { repository.getLocalCalendars() }.onSuccess { cals ->
|
||||
ownLocalCalendars = cals.filter { it.owned && !it.group }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveProfile(savedLabel: String) {
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
repository.updateProfile(
|
||||
displayName = displayName.trim().ifEmpty { null },
|
||||
username = null,
|
||||
email = email.trim(),
|
||||
)
|
||||
}
|
||||
.onSuccess { profileMessage = savedLabel }
|
||||
.onFailure { profileMessage = it.message }
|
||||
}
|
||||
}
|
||||
|
||||
fun changePrivateVisibility(value: String) {
|
||||
privateVisibility = value
|
||||
viewModelScope.launch { runCatching { repository.updatePrivateVisibility(value) } }
|
||||
}
|
||||
|
||||
fun changeGroupVisible(id: Int) {
|
||||
groupVisibleId = id
|
||||
viewModelScope.launch { runCatching { repository.updateGroupVisibleCalendar(id.takeIf { it != 0 }) } }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user