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