OXYGENHUB
# bd1d3ad70c70 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / dashboard / DashboardViewModel.kt
4.1 KiB · last changed The hub's living backgrounds in the app: the mark at sign-in, Your sky on the dashboard, live runs pulse (refs #23) 8 days ago source blame history raw
1
package io.oxygenhub.android.ui.dashboard
2
3
import androidx.lifecycle.ViewModel
4
import io.oxygenhub.android.api.Avatar
5
import io.oxygenhub.android.api.HubApi
6
import io.oxygenhub.android.api.Repository
7
import io.oxygenhub.android.api.User
8
import io.oxygenhub.android.api.UserSky
9
import io.oxygenhub.android.api.UserProposals
10
import io.oxygenhub.android.auth.Session
11
import io.oxygenhub.android.ui.common.Avatars
12
import io.oxygenhub.android.ui.common.Load
13
import io.oxygenhub.android.ui.common.load
14
import kotlinx.coroutines.CancellationException
15
import kotlinx.coroutines.Job
16
import kotlinx.coroutines.async
17
import kotlinx.coroutines.coroutineScope
18
import kotlinx.coroutines.flow.MutableStateFlow
19
import kotlinx.coroutines.flow.StateFlow
20
import kotlinx.coroutines.flow.asStateFlow
21
import java.util.Calendar
22
23
data class Dashboard(
24
    val greeting: String,
25
    /** The account, or null when the token may not read it (no `user:read`). */
26
    val user: User?,
27
    /** The account's avatar (#13), or null while the hub has none this app can draw. */
28
    val avatar: Avatar? = null,
29
    /** Whose avatar: the signed-in account's name. */
30
    val username: String = "",
31
    /** Proposals to review and the account's own (#18); null when the hub cannot say. */
32
    val proposals: UserProposals? = null,
33
    /** The newest-updated repositories: the user's own, or everyone's when they own none. */
34
    val recent: List<Repository>,
35
    val recentAreYours: Boolean,
36
    val repositoryCount: Int,
37
    /** Your sky (#23), the backdrop's data; null when the hub cannot give it. */
38
    val sky: UserSky? = null,
39
)
40
41
class DashboardViewModel(
42
    private val api: HubApi,
43
    private val session: Session,
44
    private val hourOfDay: () -> Int = { Calendar.getInstance().get(Calendar.HOUR_OF_DAY) },
45
) : ViewModel() {
46
    private val _state = MutableStateFlow<Load<Dashboard>>(Load.Loading)
47
    val state: StateFlow<Load<Dashboard>> = _state.asStateFlow()
48
49
    private var job: Job? = null
50
51
    init {
52
        refresh()
53
    }
54
55
    fun refresh() {
56
        job = load(_state, job) { fetch() }
57
    }
58
59
    private suspend fun fetch(): Dashboard = coroutineScope {
60
        // The account is a nicety (a display name); the repositories are the page.
61
        val user = async {
62
            try {
63
                api.user()
64
            } catch (e: CancellationException) {
65
                throw e
66
            } catch (_: Exception) {
67
                null
68
            }
69
        }
70
        // As are the proposals: a hub without the call shows none.
71
        val proposals = async {
72
            try {
73
                api.userProposals()
74
            } catch (e: CancellationException) {
75
                throw e
76
            } catch (_: Exception) {
77
                null
78
            }
79
        }
80
        // As is the sky behind it all.
81
        val sky = async {
82
            try {
83
                api.userSky()
84
            } catch (e: CancellationException) {
85
                throw e
86
            } catch (_: Exception) {
87
                null
88
            }
89
        }
90
        // As is the avatar: its failure leaves the last one, or the initial.
91
        val avatar = async { Avatars.load(api, session.username) }
92
        val repos = api.repositories()
93
        val me = user.await()
94
        val username = me?.username ?: session.username
95
        val mine = repos.filter { it.owner == username }
96
        val pool = mine.ifEmpty { repos }
97
        Dashboard(
98
            greeting = greeting(hourOfDay(), me?.shownName ?: session.username),
99
            user = me,
100
            avatar = avatar.await(),
101
            username = username,
102
            proposals = proposals.await(),
103
            recent = pool.sortedByDescending { it.updatedMs }.take(RECENT),
104
            recentAreYours = mine.isNotEmpty(),
105
            repositoryCount = mine.size,
106
            sky = sky.await(),
107
        )
108
    }
109
110
    companion object {
111
        const val RECENT = 5
112
113
        fun greeting(hour: Int, name: String): String {
114
            val part = when (hour) {
115
                in 5..11 -> "Good morning"
116
                in 12..17 -> "Good afternoon"
117
                else -> "Good evening"
118
            }
119
            return "$part, $name"
120
        }
121
    }
122
}