feat: add AuthViewModel, ScheduleViewModel, and EventDetailViewModel with StateFlow
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package ch.parano.myice.auth
|
||||
|
||||
interface OAuthClient {
|
||||
fun redirectUri(): String
|
||||
suspend fun authenticate(loginUrl: String): String?
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package ch.parano.myice.viewmodel
|
||||
|
||||
import ch.parano.myice.auth.AuthSettings
|
||||
import ch.parano.myice.auth.OAuthClient
|
||||
import ch.parano.myice.network.ApiConfig
|
||||
import ch.parano.myice.network.ApiService
|
||||
import ch.parano.myice.network.AuthTokenHolder
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
sealed class AuthState {
|
||||
data object Loading : AuthState()
|
||||
data class Authenticated(val email: String) : AuthState()
|
||||
data object Unauthenticated : AuthState()
|
||||
}
|
||||
|
||||
class AuthViewModel(
|
||||
private val apiService: ApiService,
|
||||
private val authSettings: AuthSettings,
|
||||
private val oauthClient: OAuthClient,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow<AuthState>(AuthState.Loading)
|
||||
val state: StateFlow<AuthState> = _state.asStateFlow()
|
||||
|
||||
fun init() {
|
||||
viewModelScope.launch {
|
||||
val token = authSettings.getStoredToken()
|
||||
if (token != null) {
|
||||
AuthTokenHolder.token = token
|
||||
try {
|
||||
val userInfo = apiService.getUserInfo()
|
||||
_state.value = AuthState.Authenticated(userInfo.email)
|
||||
} catch (e: Exception) {
|
||||
authSettings.clear()
|
||||
_state.value = AuthState.Unauthenticated
|
||||
}
|
||||
} else {
|
||||
_state.value = AuthState.Unauthenticated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login() {
|
||||
viewModelScope.launch {
|
||||
_state.value = AuthState.Loading
|
||||
try {
|
||||
val loginUrl = "${ApiConfig.baseUrl}/login?redirect_uri=" +
|
||||
oauthClient.redirectUri()
|
||||
val token = oauthClient.authenticate(loginUrl)
|
||||
if (token != null) {
|
||||
authSettings.setToken(token)
|
||||
val userInfo = apiService.getUserInfo()
|
||||
_state.value = AuthState.Authenticated(userInfo.email)
|
||||
} else {
|
||||
_state.value = AuthState.Unauthenticated
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.value = AuthState.Unauthenticated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
authSettings.clear()
|
||||
_state.value = AuthState.Unauthenticated
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package ch.parano.myice.viewmodel
|
||||
|
||||
import ch.parano.myice.models.EventDetail
|
||||
import ch.parano.myice.network.ApiService
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class EventDetailState(
|
||||
val eventDetail: EventDetail? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
class EventDetailViewModel(
|
||||
private val apiService: ApiService,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(EventDetailState(isLoading = false))
|
||||
val state: StateFlow<EventDetailState> = _state.asStateFlow()
|
||||
|
||||
fun loadEventDetail(gameId: String, account: String) {
|
||||
viewModelScope.launch {
|
||||
_state.value = EventDetailState(isLoading = true)
|
||||
try {
|
||||
val detail = apiService.getGameDetail(gameId, account)
|
||||
_state.value = EventDetailState(eventDetail = detail, isLoading = false)
|
||||
} catch (e: Exception) {
|
||||
_state.value = EventDetailState(
|
||||
error = "Failed to load event detail: ${e.message}",
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package ch.parano.myice.viewmodel
|
||||
|
||||
import ch.parano.myice.auth.AuthSettings
|
||||
import ch.parano.myice.cache.ScheduleCache
|
||||
import ch.parano.myice.filter.TypeFilter
|
||||
import ch.parano.myice.filter.distinctAgegroups
|
||||
import ch.parano.myice.filter.distinctSubgroups
|
||||
import ch.parano.myice.filter.filterEvents
|
||||
import ch.parano.myice.models.Account
|
||||
import ch.parano.myice.models.Event
|
||||
import ch.parano.myice.network.ApiService
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class ScheduleState(
|
||||
val accounts: List<Account> = emptyList(),
|
||||
val events: List<Event> = emptyList(),
|
||||
val selectedAccount: String? = null,
|
||||
val selectedAgegroup: String? = null,
|
||||
val selectedSubgroup: String? = null,
|
||||
val typeFilter: TypeFilter = TypeFilter.DEFAULT,
|
||||
val isLoading: Boolean = false,
|
||||
val isRefreshing: Boolean = false,
|
||||
val error: String? = null,
|
||||
val lastUpdated: Long? = null,
|
||||
) {
|
||||
val filteredEvents: List<Event>
|
||||
get() = filterEvents(events, selectedAgegroup, selectedSubgroup, typeFilter)
|
||||
|
||||
val agegroups: List<String>
|
||||
get() = events.distinctAgegroups()
|
||||
|
||||
val subgroups: List<String>
|
||||
get() = if (selectedAgegroup == null) emptyList()
|
||||
else events.filter { it.agegroup == selectedAgegroup }.distinctSubgroups()
|
||||
}
|
||||
|
||||
class ScheduleViewModel(
|
||||
private val apiService: ApiService,
|
||||
private val cache: ScheduleCache,
|
||||
private val authSettings: AuthSettings? = null,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(ScheduleState())
|
||||
val state: StateFlow<ScheduleState> = _state.asStateFlow()
|
||||
|
||||
fun loadAccounts() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val accounts = apiService.getAccounts()
|
||||
val storedAccount = authSettings?.getStoredAccount()
|
||||
var selected = storedAccount
|
||||
|
||||
if (selected == null || accounts.none { it.name == selected }) {
|
||||
selected = accounts.firstOrNull()?.name
|
||||
}
|
||||
|
||||
val (cachedAgegroup, cachedSubgroup) = if (selected != null) {
|
||||
cache.loadFilters(selected)
|
||||
} else null to null
|
||||
|
||||
val cachedTypeFilter = if (selected != null) {
|
||||
cache.loadTypeFilter(selected)?.let { name ->
|
||||
runCatching { TypeFilter.valueOf(name) }.getOrNull()
|
||||
} ?: TypeFilter.DEFAULT
|
||||
} else TypeFilter.DEFAULT
|
||||
|
||||
_state.value = _state.value.copy(
|
||||
accounts = accounts,
|
||||
selectedAccount = selected,
|
||||
selectedAgegroup = cachedAgegroup,
|
||||
selectedSubgroup = cachedSubgroup,
|
||||
typeFilter = cachedTypeFilter,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(error = "Failed to load accounts: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCachedSchedule() {
|
||||
val account = _state.value.selectedAccount ?: return
|
||||
|
||||
_state.value = _state.value.copy(isLoading = true, error = null)
|
||||
|
||||
val cachedEvents = cache.loadEvents(account)
|
||||
if (cachedEvents != null) {
|
||||
_state.value = _state.value.copy(
|
||||
events = cachedEvents,
|
||||
lastUpdated = cache.getLastUpdated(account),
|
||||
isLoading = false,
|
||||
)
|
||||
} else {
|
||||
_state.value = _state.value.copy(events = emptyList(), isLoading = false)
|
||||
}
|
||||
|
||||
if (_state.value.events.isEmpty()) {
|
||||
refreshSchedule()
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshSchedule() {
|
||||
val account = _state.value.selectedAccount ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
_state.value = _state.value.copy(isRefreshing = true, error = null)
|
||||
try {
|
||||
val allEvents = apiService.getSchedule(account)
|
||||
cache.saveEvents(account, allEvents)
|
||||
_state.value = _state.value.copy(
|
||||
events = allEvents,
|
||||
isRefreshing = false,
|
||||
lastUpdated = cache.getLastUpdated(account),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(
|
||||
isRefreshing = false,
|
||||
error = "Failed to refresh schedule: ${e.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAccount(account: String) {
|
||||
authSettings?.setAccount(account)
|
||||
val (cachedAgegroup, cachedSubgroup) = cache.loadFilters(account)
|
||||
val cachedTypeFilter = cache.loadTypeFilter(account)?.let { name ->
|
||||
runCatching { TypeFilter.valueOf(name) }.getOrNull()
|
||||
} ?: TypeFilter.DEFAULT
|
||||
|
||||
_state.value = _state.value.copy(
|
||||
selectedAccount = account,
|
||||
selectedAgegroup = cachedAgegroup,
|
||||
selectedSubgroup = cachedSubgroup,
|
||||
typeFilter = cachedTypeFilter,
|
||||
)
|
||||
loadCachedSchedule()
|
||||
}
|
||||
|
||||
fun setAgegroup(agegroup: String?) {
|
||||
val account = _state.value.selectedAccount
|
||||
if (account != null) {
|
||||
cache.saveFilters(account, agegroup, null)
|
||||
}
|
||||
_state.value = _state.value.copy(selectedAgegroup = agegroup, selectedSubgroup = null)
|
||||
}
|
||||
|
||||
fun setSubgroup(subgroup: String?) {
|
||||
val account = _state.value.selectedAccount
|
||||
if (account != null) {
|
||||
cache.saveFilters(account, _state.value.selectedAgegroup, subgroup)
|
||||
}
|
||||
_state.value = _state.value.copy(selectedSubgroup = subgroup)
|
||||
}
|
||||
|
||||
fun setTypeFilter(filter: TypeFilter) {
|
||||
val account = _state.value.selectedAccount
|
||||
if (account != null) {
|
||||
cache.saveTypeFilter(account, filter.name)
|
||||
}
|
||||
_state.value = _state.value.copy(typeFilter = filter)
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
cache.clearAllCache()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user