OXYGENHUB
# 0ca942a9a1e4 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / code / CodeViewModel.kt
4.5 KiB · last changed Clone from the phone: the repository screen's Clone action shows the oxy clone command with a copy button (refs #3) 8 days ago source blame history raw
1
package io.oxygenhub.android.ui.code
2
3
import androidx.lifecycle.ViewModel
4
import io.oxygenhub.android.api.HubApi
5
import io.oxygenhub.android.api.TreeEntry
6
import io.oxygenhub.android.ui.common.Load
7
import io.oxygenhub.android.ui.common.load
8
import kotlinx.coroutines.Job
9
import kotlinx.coroutines.flow.MutableStateFlow
10
import kotlinx.coroutines.flow.StateFlow
11
import kotlinx.coroutines.flow.asStateFlow
12
13
/**
14
 * A repository's code (#3): a directory of its tree at a bookmark, tag or
15
 * change, folders first, and the directory's README to show below it.
16
 */
17
class CodeViewModel(
18
    private val api: HubApi,
19
    val owner: String,
20
    val name: String,
21
    /** The bookmark to start at; the repository's default when not given. */
22
    defaultRef: String? = null,
23
) : ViewModel() {
24
    /** What a directory holds, and its README's text when it has one. */
25
    data class Dir(val ref: String, val path: String, val entries: List<TreeEntry>, val readme: Readme?)
26
27
    data class Readme(val path: String, val text: String)
28
29
    private val _ref = MutableStateFlow(defaultRef.orEmpty())
30
    val ref: StateFlow<String> = _ref.asStateFlow()
31
32
    private val _path = MutableStateFlow("")
33
    val path: StateFlow<String> = _path.asStateFlow()
34
35
    private val _state = MutableStateFlow<Load<Dir>>(Load.Loading)
36
    val state: StateFlow<Load<Dir>> = _state.asStateFlow()
37
38
    /** The bookmarks and tags to browse at, fetched once the picker opens. */
39
    private val _refs = MutableStateFlow<Load<Pair<List<String>, List<String>>>>(Load.Loading)
40
    val refs: StateFlow<Load<Pair<List<String>, List<String>>>> = _refs.asStateFlow()
41
42
    private var job: Job? = null
43
    private var refsJob: Job? = null
44
45
    init {
46
        refresh()
47
    }
48
49
    fun open(path: String) {
50
        if (path == _path.value) return
51
        _path.value = path
52
        _state.value = Load.Loading
53
        refresh()
54
    }
55
56
    /** One directory up; false at the top. */
57
    fun up(): Boolean {
58
        val p = _path.value
59
        if (p.isEmpty()) return false
60
        open(p.substringBeforeLast('/', ""))
61
        return true
62
    }
63
64
    fun setRef(ref: String) {
65
        if (ref == _ref.value) return
66
        _ref.value = ref
67
        _state.value = Load.Loading
68
        refresh()
69
    }
70
71
    fun loadRefs() {
72
        if (_refs.value is Load.Ready) return
73
        refsJob = load(_refs, refsJob) { api.refs(owner, name) }
74
    }
75
76
    private var cloneUrl: String? = null
77
78
    /** The repository's clone address (#3), fetched once. */
79
    suspend fun cloneUrl(): String = cloneUrl ?: api.repository(owner, name).cloneUrl.also { cloneUrl = it }
80
81
    fun refresh() {
82
        val path = _path.value
83
        job = load(_state, job) {
84
            val ref = _ref.value.ifEmpty { api.repository(owner, name).also { cloneUrl = it.cloneUrl }.defaultBookmark.also { _ref.value = it } }
85
            val entries = order(api.tree(owner, name, ref, path))
86
            val readme = readmeOf(entries)?.let { e ->
87
                // A README that will not load is left out, not an error.
88
                runCatching { Readme(e.path, api.file(owner, name, ref, e.path)) }.getOrNull()
89
            }
90
            Dir(ref, path, entries, readme)
91
        }
92
    }
93
94
    companion object {
95
        /** Folders first, then files, each by name as a person reads them. */
96
        fun order(entries: List<TreeEntry>): List<TreeEntry> =
97
            entries.sortedWith(compareBy<TreeEntry> { !it.isDir }.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name })
98
99
        /** The README a directory shows: Markdown first. */
100
        fun readmeOf(entries: List<TreeEntry>): TreeEntry? {
101
            val files = entries.filter { !it.isDir }
102
            return listOf("README.md", "readme.md", "README.markdown", "README", "README.txt")
103
                .firstNotNullOfOrNull { n -> files.firstOrNull { it.name.equals(n, ignoreCase = false) } }
104
                ?: files.firstOrNull { it.name.lowercase().startsWith("readme") }
105
        }
106
    }
107
}
108
109
/** A file at a reference: its text, to show highlighted, or rendered when it is Markdown. */
110
class FileViewModel(
111
    private val api: HubApi,
112
    val owner: String,
113
    val name: String,
114
    val ref: String,
115
    val path: String,
116
) : ViewModel() {
117
    private val _state = MutableStateFlow<Load<String>>(Load.Loading)
118
    val state: StateFlow<Load<String>> = _state.asStateFlow()
119
    private var job: Job? = null
120
121
    init {
122
        refresh()
123
    }
124
125
    fun refresh() {
126
        job = load(_state, job) { api.file(owner, name, ref, path) }
127
    }
128
129
    val isMarkdown: Boolean get() = path.lowercase().let { it.endsWith(".md") || it.endsWith(".markdown") }
130
}