feat: full Compose UI (auth, calendar, events, accounts, settings, profile)

- Root navigation (server setup -> login+2FA -> calendar) with settings-driven theme
- CalendarViewModel mirroring iOS CalendarStore: range-based caching,
  background prefetch, hidden/banished calendar filters, event CRUD
- Five calendar views: Month, Week, Day, Quarter, Agenda (shared time grid)
- Event detail + editor sheets (platform date/time pickers, color override,
  writable-calendar picker) across local/caldav/google/homeassistant
- Calendar filter sheet (per-calendar show/hide)
- Accounts screen: add/delete CalDAV, local, iCal, Home Assistant; list Google
- Settings: view/week-start/language/colors/contrast/hour-height/cache range,
  synced to server
- Profile: email, password change, 2FA enable/disable
- gradlew build (lint + tests + assemble) passes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido Schmit
2026-05-31 12:22:56 +02:00
parent 676b7ee2c6
commit aff6cef493
26 changed files with 3129 additions and 35 deletions

View File

@@ -0,0 +1,79 @@
package com.scarriffle.calendarr.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.scarriffle.calendarr.data.CalendarRepository
import com.scarriffle.calendarr.data.CredentialStore
import com.scarriffle.calendarr.data.SettingsStore
import com.scarriffle.calendarr.data.remote.ApiProvider
import com.scarriffle.calendarr.domain.model.AppSettings
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
enum class AppRoute { SETUP, LOGIN, MAIN }
@HiltViewModel
class MainViewModel @Inject constructor(
private val credentialStore: CredentialStore,
private val settingsStore: SettingsStore,
private val repository: CalendarRepository,
private val apiProvider: ApiProvider,
) : ViewModel() {
private val _route = MutableStateFlow(computeRoute())
val route: StateFlow<AppRoute> = _route.asStateFlow()
private val _settings = MutableStateFlow(settingsStore.loadSettings())
val settings: StateFlow<AppSettings> = _settings.asStateFlow()
val serverUrl: String get() = credentialStore.serverUrl ?: ""
val username: String get() = credentialStore.username ?: ""
val isAdmin: Boolean get() = credentialStore.isAdmin
init {
if (_route.value == AppRoute.MAIN) refreshSettings()
}
private fun computeRoute(): AppRoute = when {
!credentialStore.isConfigured -> AppRoute.SETUP
!credentialStore.isLoggedIn -> AppRoute.LOGIN
else -> AppRoute.MAIN
}
fun onServerConfigured() { _route.value = computeRoute() }
fun onLoggedIn() {
_route.value = AppRoute.MAIN
refreshSettings()
}
fun logout() {
credentialStore.clearToken()
_route.value = computeRoute()
}
fun switchServer() {
credentialStore.clearAll()
apiProvider.invalidate()
_route.value = computeRoute()
}
/** Pull appearance settings from the server, caching them locally. */
fun refreshSettings() {
viewModelScope.launch {
runCatching { repository.getSettings() }.onSuccess { s ->
settingsStore.saveSettings(s)
_settings.value = s
}
}
}
fun applyLocalSettings(s: AppSettings) {
settingsStore.saveSettings(s)
_settings.value = s
}
}