1package com.parseable.android.ui.screens.streams
3import androidx.lifecycle.ViewModel
4import androidx.lifecycle.viewModelScope
5import com.parseable.android.data.local.FavoriteStream
6import com.parseable.android.data.local.FavoriteStreamDao
7import com.parseable.android.data.model.AboutInfo
8import com.parseable.android.data.model.ApiResult
9import com.parseable.android.data.model.LogStream
10import com.parseable.android.data.repository.ParseableRepository
11import com.parseable.android.data.repository.SettingsRepository
12import dagger.hilt.android.lifecycle.HiltViewModel
13import kotlinx.coroutines.CancellationException
14import kotlinx.coroutines.Job
15import kotlinx.coroutines.async
16import kotlinx.coroutines.coroutineScope
17import kotlinx.coroutines.channels.Channel
18import kotlinx.coroutines.flow.MutableStateFlow
19import kotlinx.coroutines.flow.StateFlow
20import kotlinx.coroutines.flow.asStateFlow
21import kotlinx.coroutines.flow.receiveAsFlow
22import kotlinx.coroutines.flow.update
23import kotlinx.coroutines.launch
24import kotlinx.coroutines.sync.Semaphore
25import kotlinx.coroutines.sync.withPermit
26import javax.inject.Inject
28data class StreamsState(
29 val streams: List<LogStream> = emptyList(),
30 val streamStats: Map<String, StreamsViewModel.StreamStatsUi> = emptyMap(),
31 val failedStats: Set<String> = emptySet(),
32 val aboutInfo: AboutInfo? = null,
33 val isLoading: Boolean = false,
34 val error: String? = null,
35 val favoriteNames: Set<String> = emptySet(),
39class StreamsViewModel @Inject constructor(
40 private val repository: ParseableRepository,
41 private val settingsRepository: SettingsRepository,
42 private val favoriteDao: FavoriteStreamDao,
45 data class StreamStatsUi(
46 val eventCount: Long? = null,
47 val ingestionSize: String? = null,
48 val storageSize: String? = null,
51 private val _state = MutableStateFlow(StreamsState())
52 val state: StateFlow<StreamsState> = _state.asStateFlow()
54 private val _snackbarEvent = Channel<String>(Channel.BUFFERED)
55 val snackbarEvent = _snackbarEvent.receiveAsFlow()
58 viewModelScope.launch {
59 favoriteDao.getAllNames().collect { names ->
60 _state.update { it.copy(favoriteNames = names.toSet()) }
65 fun toggleFavorite(streamName: String) {
66 viewModelScope.launch {
67 if (streamName in _state.value.favoriteNames) {
68 favoriteDao.deleteByName(streamName)
69 _snackbarEvent.send("Removed from favorites")
71 favoriteDao.insert(FavoriteStream(streamName = streamName))
72 _snackbarEvent.send("Added to favorites")
78 viewModelScope.launch {
79 _state.update { it.copy(isLoading = true, error = null) }
82 // Fetch about info and streams in parallel
83 val (aboutResult, streamsResult) = coroutineScope {
84 val aboutDeferred = async { repository.getAbout() }
85 val streamsDeferred = async { repository.listStreams(forceRefresh = true) }
86 aboutDeferred.await() to streamsDeferred.await()
89 if (aboutResult is ApiResult.Success) {
90 _state.update { it.copy(aboutInfo = aboutResult.data) }
93 when (streamsResult) {
94 is ApiResult.Success -> {
97 streams = streamsResult.data,
101 // Load stats for each stream in parallel
102 loadStreamStats(streamsResult.data)
104 is ApiResult.Error -> {
108 error = streamsResult.userMessage,
113 } catch (e: CancellationException) {
114 _state.update { it.copy(isLoading = false) }
116 } catch (e: Exception) {
118 it.copy(isLoading = false, error = e.message ?: "Failed to load streams")
124 private val statsSemaphore = Semaphore(8)
125 private var statsJob: Job? = null
127 private fun loadStreamStats(streams: List<LogStream>) {
129 statsJob = viewModelScope.launch {
130 streams.forEach { stream ->
132 statsSemaphore.withPermit {
133 loadSingleStreamStats(stream.name)
140 fun retryStats(streamName: String) {
141 viewModelScope.launch {
142 _state.update { it.copy(failedStats = it.failedStats - streamName) }
143 statsSemaphore.withPermit {
144 loadSingleStreamStats(streamName)
149 private suspend fun loadSingleStreamStats(streamName: String) {
150 when (val result = repository.getStreamStats(streamName)) {
151 is ApiResult.Success -> {
152 val stats = result.data
153 val ui = StreamStatsUi(
154 eventCount = stats.ingestion?.count
155 ?: stats.ingestion?.lifetimeCount,
156 ingestionSize = stats.ingestion?.size
157 ?: stats.ingestion?.lifetimeSize,
158 storageSize = stats.storage?.size
159 ?: stats.storage?.lifetimeSize,
163 streamStats = it.streamStats + (streamName to ui),
164 failedStats = it.failedStats - streamName,
168 is ApiResult.Error -> {
170 it.copy(failedStats = it.failedStats + streamName)
176 suspend fun logout() {
177 settingsRepository.clearConfig()
178 repository.clearCredentials()
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list