From 4a44c20b1258e1435405cd11470ed8413130479d Mon Sep 17 00:00:00 2001 From: Guido Schmit Date: Sun, 31 May 2026 21:54:19 +0200 Subject: [PATCH] feat: Android Ersteller-Anzeige + Privat-Flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Event-Detail zeigt "Erstellt von" (wenn != ich) + Privat-Hinweis; Editor hat Privat-Toggle (nur lokale Kalender, durch saveEvent/Repo durchgereicht). Login speichert userId + displayName (CredentialStore) für Vergleiche. Co-Authored-By: Claude Opus 4.8 --- .../calendarr/data/CalendarRepository.kt | 7 ++++++- .../scarriffle/calendarr/data/CredentialStore.kt | 14 +++++++++++++- .../scarriffle/calendarr/domain/model/CalEvent.kt | 8 ++++++-- .../main/java/com/scarriffle/calendarr/ui/L10n.kt | 4 ++-- .../calendarr/ui/calendar/CalendarScreen.kt | 5 +++-- .../calendarr/ui/calendar/CalendarViewModel.kt | 14 +++++++++----- .../calendarr/ui/event/EventDetailScreen.kt | 9 +++++++++ .../calendarr/ui/event/EventEditorSheet.kt | 14 ++++++++++++-- 8 files changed, 60 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/scarriffle/calendarr/data/CalendarRepository.kt b/app/src/main/java/com/scarriffle/calendarr/data/CalendarRepository.kt index 59e76cf..0073599 100644 --- a/app/src/main/java/com/scarriffle/calendarr/data/CalendarRepository.kt +++ b/app/src/main/java/com/scarriffle/calendarr/data/CalendarRepository.kt @@ -46,6 +46,9 @@ class CalendarRepository @Inject constructor( ) { private val api get() = apiProvider.api() + /** Current logged-in user id (0 if unknown) — for creator/owner comparisons. */ + val currentUserId: Int get() = credentialStore.userId + private suspend fun guarded(block: suspend () -> T): T = withContext(Dispatchers.IO) { try { block() @@ -93,9 +96,11 @@ class CalendarRepository @Inject constructor( val user = json.optJSONObject("user") val uname = user?.optString("username") ?: username val isAdmin = user?.optBoolean("is_admin", false) ?: false + val uid = user?.optInt("id", 0) ?: 0 + val displayName = user?.optString("display_name")?.takeIf { it.isNotBlank() } ?: uname credentialStore.serverUrl = ApiProvider.normalize(baseUrl) - credentialStore.saveLogin(token, uname, isAdmin) + credentialStore.saveLogin(token, uname, isAdmin, uid, displayName) apiProvider.invalidate() LoginResult(token, uname, isAdmin) } diff --git a/app/src/main/java/com/scarriffle/calendarr/data/CredentialStore.kt b/app/src/main/java/com/scarriffle/calendarr/data/CredentialStore.kt index d9c5ab6..07207b4 100644 --- a/app/src/main/java/com/scarriffle/calendarr/data/CredentialStore.kt +++ b/app/src/main/java/com/scarriffle/calendarr/data/CredentialStore.kt @@ -46,17 +46,27 @@ class CredentialStore @Inject constructor( get() = prefs.getBoolean(KEY_IS_ADMIN, false) set(value) = prefs.edit().putBoolean(KEY_IS_ADMIN, value).apply() + var userId: Int + get() = prefs.getInt(KEY_USER_ID, 0) + set(value) = prefs.edit().putInt(KEY_USER_ID, value).apply() + + var displayName: String? + get() = prefs.getString(KEY_DISPLAY_NAME, null) + set(value) = prefs.edit().putString(KEY_DISPLAY_NAME, value).apply() + /** True once a server URL has been entered (setup step complete). */ val isConfigured: Boolean get() = !serverUrl.isNullOrBlank() /** True once we hold an auth token (logged in). */ val isLoggedIn: Boolean get() = !token.isNullOrBlank() - fun saveLogin(token: String, username: String, isAdmin: Boolean) { + fun saveLogin(token: String, username: String, isAdmin: Boolean, userId: Int = 0, displayName: String? = null) { prefs.edit() .putString(KEY_TOKEN, token) .putString(KEY_USERNAME, username) .putBoolean(KEY_IS_ADMIN, isAdmin) + .putInt(KEY_USER_ID, userId) + .putString(KEY_DISPLAY_NAME, displayName ?: username) .apply() } @@ -79,5 +89,7 @@ class CredentialStore @Inject constructor( const val KEY_TOKEN = "auth_token" const val KEY_USERNAME = "username" const val KEY_IS_ADMIN = "is_admin" + const val KEY_USER_ID = "user_id" + const val KEY_DISPLAY_NAME = "display_name" } } diff --git a/app/src/main/java/com/scarriffle/calendarr/domain/model/CalEvent.kt b/app/src/main/java/com/scarriffle/calendarr/domain/model/CalEvent.kt index 77d9ccb..e5ccd8c 100644 --- a/app/src/main/java/com/scarriffle/calendarr/domain/model/CalEvent.kt +++ b/app/src/main/java/com/scarriffle/calendarr/domain/model/CalEvent.kt @@ -30,13 +30,16 @@ data class CalEvent( // Only set in the group combined view: val owner: EventPerson? = null, val isGroupEvent: Boolean = false, + val displayColor: String? = null, ) { /** - * Per-event override colour, then the calendar's colour, then a stable + * Group view supplies a server-resolved colour (display_color); otherwise + * per-event override colour, then the calendar's colour, then a stable * per-calendar palette colour (so events never collapse to one default). */ val effectiveColor: String - get() = color?.takeIf { it.isNotBlank() } + get() = displayColor?.takeIf { it.isNotBlank() } + ?: color?.takeIf { it.isNotBlank() } ?: calendarColor.takeIf { it.isNotBlank() } ?: fallbackColorFor("$source:$calendarId") @@ -113,6 +116,7 @@ data class CalEvent( isPrivate = json.optBoolean("private", false), owner = personFrom(json, "owner"), isGroupEvent = json.optBoolean("is_group_event", false), + displayColor = json.strOrNull("display_color"), ) } } diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt b/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt index 30de0b8..8efdb94 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt @@ -87,7 +87,7 @@ object L10n { "twofa.code_placeholder" to "6-stelliger Code", "twofa.activate" to "Aktivieren", "twofa.disable_title" to "2FA deaktivieren", "twofa.password_placeholder" to "Passwort", "twofa.disable" to "Deaktivieren", - "event.title_placeholder" to "Titel", "event.allday" to "Ganztägig", + "event.title_placeholder" to "Titel", "event.allday" to "Ganztägig", "event.private" to "Privat", "event.created_by" to "Erstellt von", "event.start" to "Start", "event.end" to "Ende", "event.location" to "Ort", "event.description" to "Beschreibung", "event.calendar_section" to "Kalender", "event.no_writable" to "Keine beschreibbaren Kalender vorhanden", @@ -188,7 +188,7 @@ object L10n { "twofa.code_placeholder" to "6-digit code", "twofa.activate" to "Activate", "twofa.disable_title" to "Disable 2FA", "twofa.password_placeholder" to "Password", "twofa.disable" to "Disable", - "event.title_placeholder" to "Title", "event.allday" to "All-day", + "event.title_placeholder" to "Title", "event.allday" to "All-day", "event.private" to "Private", "event.created_by" to "Created by", "event.start" to "Start", "event.end" to "End", "event.location" to "Location", "event.description" to "Description", "event.calendar_section" to "Calendar", "event.no_writable" to "No writable calendars available", diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt index 98d86f4..e43f207 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt @@ -188,6 +188,7 @@ fun CalendarScreen( if (ev != null) { EventDetailScreen( event = ev, + currentUserId = vm.currentUserId, onClose = { detailEvent = null }, onEdit = { detailEvent = null @@ -210,8 +211,8 @@ fun CalendarScreen( request = req, writableCalendars = state.writableCalendars, onDismiss = { editor = null }, - onSave = { cal, title, start, end, allDay, location, desc, color -> - vm.saveEvent(cal, req.existing, title, start, end, allDay, location, desc, color) { error -> + onSave = { cal, title, start, end, allDay, location, desc, color, isPrivate -> + vm.saveEvent(cal, req.existing, title, start, end, allDay, location, desc, color, isPrivate) { error -> if (error == null) editor = null } }, diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt index b669a69..17507ab 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt @@ -48,6 +48,9 @@ class CalendarViewModel @Inject constructor( private val zone: ZoneId = ZoneId.systemDefault() + /** Current user id (for creator/owner comparisons in the UI). */ + val currentUserId: Int get() = repository.currentUserId + /** Serializes network loads so overlapping fetches don't thrash the UI. */ private val loadMutex = Mutex() @@ -298,20 +301,21 @@ class CalendarViewModel @Inject constructor( location: String, description: String, color: String?, + isPrivate: Boolean, onResult: (String?) -> Unit, ) { viewModelScope.launch { val result = runCatching { if (existing != null && existing.source == calendar.source) { when (existing.source) { - "local" -> repository.updateLocalEvent(existing.id, title, start, end, isAllDay, location, description, color) + "local" -> repository.updateLocalEvent(existing.id, title, start, end, isAllDay, location, description, color, isPrivate) "caldav" -> repository.updateCalDAVEvent(existing.id, existing.url, calendar.numericId, title, start, end, isAllDay, location, description, color) "homeassistant" -> repository.updateHAEvent(calendar.numericId, existing.id, title, start, end, isAllDay, location, description) "google" -> repository.updateGoogleEvent(calendar.numericId, existing.id, title, start, end, isAllDay, location, description) - else -> createForSource(calendar, title, start, end, isAllDay, location, description, color) + else -> createForSource(calendar, title, start, end, isAllDay, location, description, color, isPrivate) } } else { - createForSource(calendar, title, start, end, isAllDay, location, description, color) + createForSource(calendar, title, start, end, isAllDay, location, description, color, isPrivate) } } result.onSuccess { afterMutation(); onResult(null) } @@ -321,10 +325,10 @@ class CalendarViewModel @Inject constructor( private suspend fun createForSource( calendar: WritableCalendar, title: String, start: Instant, end: Instant, - isAllDay: Boolean, location: String, description: String, color: String?, + isAllDay: Boolean, location: String, description: String, color: String?, isPrivate: Boolean, ) { when (calendar.source) { - "local" -> repository.createLocalEvent(calendar.numericId, title, start, end, isAllDay, location, description, color) + "local" -> repository.createLocalEvent(calendar.numericId, title, start, end, isAllDay, location, description, color, isPrivate) "caldav" -> repository.createCalDAVEvent(calendar.numericId, title, start, end, isAllDay, location, description, color) "google" -> repository.createGoogleEvent(calendar.numericId, title, start, end, isAllDay, location, description) "homeassistant" -> repository.createHAEvent(calendar.numericId, title, start, end, isAllDay, location, description) diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/event/EventDetailScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/event/EventDetailScreen.kt index 584c6e8..59fdb9d 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/event/EventDetailScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/event/EventDetailScreen.kt @@ -23,7 +23,9 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Dns import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.LocationOn +import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Notes +import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Schedule import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -60,6 +62,7 @@ import com.scarriffle.calendarr.util.colorFromHex @Composable fun EventDetailScreen( event: CalEvent, + currentUserId: Int = 0, onClose: () -> Unit, onEdit: () -> Unit, onCopy: () -> Unit, @@ -109,6 +112,12 @@ fun EventDetailScreen( if (event.notes.isNotBlank()) DetailRow(Icons.Filled.Notes, event.notes) if (event.calendarName.isNotBlank()) DetailRow(Icons.Filled.CalendarMonth, event.calendarName) DetailRow(Icons.Filled.Dns, event.source.replaceFirstChar { it.uppercase() }) + event.creator?.let { c -> + if (c.id != currentUserId) { + DetailRow(Icons.Filled.Person, "${tr("event.created_by")}: ${c.displayName}") + } + } + if (event.isPrivate) DetailRow(Icons.Filled.Lock, tr("event.private")) Spacer(Modifier.height(28.dp)) OutlinedButton(onClick = onCopy, modifier = Modifier.fillMaxWidth()) { diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt b/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt index a510d32..758e01c 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt @@ -69,7 +69,7 @@ fun EventEditorSheet( request: EditorRequest, writableCalendars: List, onDismiss: () -> Unit, - onSave: (WritableCalendar, String, Instant, Instant, Boolean, String, String, String?) -> Unit, + onSave: (WritableCalendar, String, Instant, Instant, Boolean, String, String, String?, Boolean) -> Unit, ) { val zone = ZoneId.systemDefault() val context = LocalContext.current @@ -85,6 +85,7 @@ fun EventEditorSheet( var location by remember { mutableStateOf(template?.location ?: "") } var description by remember { mutableStateOf(template?.notes ?: "") } var color by remember { mutableStateOf(template?.color) } + var isPrivate by remember { mutableStateOf(template?.isPrivate ?: false) } val initialStart = template?.startDate val initialEnd = template?.endDate @@ -230,6 +231,15 @@ fun EventEditorSheet( } Spacer(Modifier.size(12.dp)) + // Private (local calendars only) + if (calendar?.source == "local") { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { + Text(tr("event.private"), style = MaterialTheme.typography.bodyLarge) + Switch(checked = isPrivate, onCheckedChange = { isPrivate = it }) + } + Spacer(Modifier.size(12.dp)) + } + // Color Text(tr("event.color_section"), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) Row(Modifier.padding(top = 6.dp), horizontalArrangement = Arrangement.spacedBy(10.dp)) { @@ -270,7 +280,7 @@ fun EventEditorSheet( start = startDate.atTime(startTime).atZone(zone).toInstant() end = endDate.atTime(endTime).atZone(zone).toInstant() } - onSave(cal, title.trim(), start, end, allDay, location.trim(), description.trim(), color) + onSave(cal, title.trim(), start, end, allDay, location.trim(), description.trim(), color, isPrivate && cal.source == "local") }, enabled = writableCalendars.isNotEmpty(), modifier = Modifier.fillMaxWidth(),