git.iko.soy/opencode-android
on f9ed438 help sign in
opencode-android/app/src/main/java/soy/iko/opencode/ui/file/FileBrowserViewModel.kt
79 lines 2.9 kB history raw
1package soy.iko.opencode.ui.file
3import androidx.lifecycle.ViewModel
4import androidx.lifecycle.viewModelScope
5import soy.iko.opencode.data.model.FileNode
6import soy.iko.opencode.di.AppContainer
7import kotlinx.coroutines.Job
8import kotlinx.coroutines.delay
9import kotlinx.coroutines.flow.MutableStateFlow
10import kotlinx.coroutines.flow.StateFlow
11import kotlinx.coroutines.flow.asStateFlow
12import kotlinx.coroutines.launch
14data class FileBrowserState(
15 val path: String = "",
16 val entries: List<FileNode> = emptyList(),
17 val query: String = "",
18 val results: List<String> = emptyList(),
19 val searching: Boolean = false,
20 val loading: Boolean = true,
21 val error: String? = null,
22) {
23 val isSearching: Boolean get() = query.isNotBlank()
26class FileBrowserViewModel(private val container: AppContainer) : ViewModel() {
28 private val api get() = container.activeConnection.value?.api
30 private val _state = MutableStateFlow(FileBrowserState())
31 val state: StateFlow<FileBrowserState> = _state.asStateFlow()
33 private var searchJob: Job? = null
35 init { open("") }
37 fun open(path: String) {
38 val client = api ?: run {
39 _state.value = _state.value.copy(loading = false, error = "Not connected")
40 return
41 }
42 _state.value = _state.value.copy(path = path, loading = true, error = null)
43 viewModelScope.launch {
44 runCatching { client.listDirectory(path) }
45 .onSuccess { entries ->
46 _state.value = _state.value.copy(
47 entries = entries.sortedWith(compareByDescending<FileNode> { it.isDirectory }.thenBy { it.name.lowercase() }),
48 loading = false,
49 )
50 }
51 .onFailure { _state.value = _state.value.copy(loading = false, error = it.message ?: "Failed to list $path") }
52 }
53 }
55 /** Navigate up one path segment. */
56 fun up() {
57 val current = _state.value.path
58 if (current.isBlank()) return
59 val parent = current.trimEnd('/').substringBeforeLast('/', missingDelimiterValue = "")
60 open(parent)
61 }
63 fun setQuery(query: String) {
64 _state.value = _state.value.copy(query = query)
65 searchJob?.cancel()
66 if (query.isBlank()) {
67 _state.value = _state.value.copy(results = emptyList(), searching = false)
68 return
69 }
70 val client = api ?: return
71 searchJob = viewModelScope.launch {
72 delay(250) // debounce
73 _state.value = _state.value.copy(searching = true)
74 runCatching { client.findFiles(query) }
75 .onSuccess { _state.value = _state.value.copy(results = it, searching = false) }
76 .onFailure { _state.value = _state.value.copy(searching = false, error = it.message) }
77 }
78 }