OXYGENHUB
# 263c6cad5e51 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / projects / ProjectsViewModel.kt
3.5 KiB · last changed Project boards on the phone: every board you can see, columns to swipe, cards that open and move 10 days ago source blame history raw
1
package io.oxygenhub.android.ui.projects
2
3
import androidx.lifecycle.ViewModel
4
import androidx.lifecycle.viewModelScope
5
import io.oxygenhub.android.api.Board
6
import io.oxygenhub.android.api.HubApi
7
import io.oxygenhub.android.api.Project
8
import io.oxygenhub.android.api.userMessage
9
import io.oxygenhub.android.ui.common.Load
10
import io.oxygenhub.android.ui.common.load
11
import kotlinx.coroutines.CancellationException
12
import kotlinx.coroutines.Job
13
import kotlinx.coroutines.async
14
import kotlinx.coroutines.awaitAll
15
import kotlinx.coroutines.coroutineScope
16
import kotlinx.coroutines.flow.MutableStateFlow
17
import kotlinx.coroutines.flow.StateFlow
18
import kotlinx.coroutines.flow.asStateFlow
19
import kotlinx.coroutines.launch
20
21
/**
22
 * The boards the account can see: its own and those of the owners of its
23
 * repositories (its organisations), open ones first, newest first.
24
 */
25
class ProjectsViewModel(private val api: HubApi, private val username: String) : ViewModel() {
26
    private val _state = MutableStateFlow<Load<List<Project>>>(Load.Loading)
27
    val state: StateFlow<Load<List<Project>>> = _state.asStateFlow()
28
    private var job: Job? = null
29
30
    init {
31
        refresh()
32
    }
33
34
    fun refresh() {
35
        job = load(_state, job) {
36
            val owners = (listOf(username) + api.repositories().map { it.owner }).distinct()
37
            val found = coroutineScope {
38
                // An owner whose boards will not load is left out, not the list.
39
                owners.map { o -> async { runCatching { api.projects(o) }.getOrDefault(emptyList()) } }.awaitAll().flatten()
40
            }
41
            order(found)
42
        }
43
    }
44
45
    companion object {
46
        fun order(projects: List<Project>): List<Project> =
47
            projects.distinctBy { it.owner to it.number }.sortedWith(compareBy<Project> { it.closed }.thenByDescending { it.updatedMs })
48
    }
49
}
50
51
/** One board: its columns and cards; a card moves to another column. */
52
class BoardViewModel(private val api: HubApi, val owner: String, val number: Long) : ViewModel() {
53
    private val _state = MutableStateFlow<Load<Board>>(Load.Loading)
54
    val state: StateFlow<Load<Board>> = _state.asStateFlow()
55
56
    private val _error = MutableStateFlow<String?>(null)
57
    val error: StateFlow<String?> = _error.asStateFlow()
58
59
    private var job: Job? = null
60
61
    init {
62
        refresh()
63
    }
64
65
    fun refresh() {
66
        job = load(_state, job) { api.board(owner, number) }
67
    }
68
69
    /** Move [card] to [column]: shown at once, put back if the hub refuses. */
70
    fun move(card: Long, column: String) {
71
        val before = (_state.value as? Load.Ready)?.value ?: return
72
        val moving = before.columns.flatMap { it.cards }.firstOrNull { it.id == card } ?: return
73
        if (before.columns.firstOrNull { c -> c.cards.any { it.id == card } }?.name == column) return
74
        _state.value = Load.Ready(
75
            before.copy(columns = before.columns.map { c ->
76
                val rest = c.cards.filter { it.id != card }
77
                if (c.name == column) c.copy(cards = rest + moving) else c.copy(cards = rest)
78
            }),
79
        )
80
        _error.value = null
81
        viewModelScope.launch {
82
            try {
83
                api.moveCard(owner, number, card, column)
84
            } catch (e: CancellationException) {
85
                throw e
86
            } catch (e: Exception) {
87
                _state.value = Load.Ready(before)
88
                _error.value = e.userMessage()
89
            }
90
        }
91
    }
92
93
    fun dismissError() {
94
        _error.value = null
95
    }
96
}