feat: Android Ersteller-Anzeige + Privat-Flag
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <T> 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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -69,7 +69,7 @@ fun EventEditorSheet(
|
||||
request: EditorRequest,
|
||||
writableCalendars: List<WritableCalendar>,
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user