3c008ec8df
- Replace SAF document picker with custom LocalBrowserDialog (File API, quick-access shortcuts, breadcrumb nav, search, folder-only listing) - Rewrite RemoteBrowserDialog as full-screen dialog with breadcrumbs, search, and new-folder creation; add navigateToBreadcrumb/createFolder to RemoteBrowserViewModel - Fix Select button cut off by navigation bar in both browsers: wrap button in Column(navigationBarsPadding()) so the button sits above the nav bar rather than behind it - Tighten icon foreground crop to remove excess black border Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
4.2 KiB
Kotlin
108 lines
4.2 KiB
Kotlin
package com.syncflow.ui.browser
|
|
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import com.syncflow.data.providers.ProviderFactory
|
|
import com.syncflow.data.repository.AccountRepository
|
|
import com.syncflow.domain.model.RemoteFile
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
import kotlinx.coroutines.Job
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
import kotlinx.coroutines.flow.update
|
|
import kotlinx.coroutines.launch
|
|
import javax.inject.Inject
|
|
|
|
data class BrowserState(
|
|
val accountId: Long = -1L,
|
|
val currentPath: String = "/",
|
|
val pathStack: List<String> = listOf("/"),
|
|
val entries: List<RemoteFile> = emptyList(),
|
|
val isLoading: Boolean = false,
|
|
val error: String? = null,
|
|
)
|
|
|
|
@HiltViewModel
|
|
class RemoteBrowserViewModel @Inject constructor(
|
|
private val accountRepository: AccountRepository,
|
|
private val providerFactory: ProviderFactory,
|
|
) : ViewModel() {
|
|
|
|
private val _state = MutableStateFlow(BrowserState())
|
|
val state = _state.asStateFlow()
|
|
|
|
private var loadJob: Job? = null
|
|
|
|
fun init(accountId: Long, startPath: String = "/") {
|
|
loadJob?.cancel()
|
|
_state.value = BrowserState(accountId = accountId, currentPath = startPath, pathStack = listOf(startPath), isLoading = true)
|
|
loadJob = loadPath(accountId, startPath)
|
|
}
|
|
|
|
fun navigateTo(path: String) {
|
|
val accountId = _state.value.accountId
|
|
loadJob?.cancel()
|
|
_state.update { s -> s.copy(currentPath = path, pathStack = s.pathStack + path, isLoading = true, entries = emptyList(), error = null) }
|
|
loadJob = loadPath(accountId, path)
|
|
}
|
|
|
|
fun navigateUp(): Boolean {
|
|
val stack = _state.value.pathStack
|
|
if (stack.size <= 1) return false
|
|
val newStack = stack.dropLast(1)
|
|
val parent = newStack.last()
|
|
loadJob?.cancel()
|
|
_state.update { it.copy(currentPath = parent, pathStack = newStack, isLoading = true, entries = emptyList(), error = null) }
|
|
loadJob = loadPath(_state.value.accountId, parent)
|
|
return true
|
|
}
|
|
|
|
fun navigateToBreadcrumb(path: String) {
|
|
val stack = _state.value.pathStack
|
|
val idx = stack.lastIndexOf(path)
|
|
val newStack = if (idx >= 0) stack.take(idx + 1) else listOf(path)
|
|
loadJob?.cancel()
|
|
_state.update { it.copy(currentPath = path, pathStack = newStack, isLoading = true, entries = emptyList(), error = null) }
|
|
loadJob = loadPath(_state.value.accountId, path)
|
|
}
|
|
|
|
fun createFolder(name: String) {
|
|
val s = _state.value
|
|
val newPath = if (s.currentPath.trimEnd('/') == "") "/$name" else "${s.currentPath.trimEnd('/')}/$name"
|
|
viewModelScope.launch {
|
|
val account = accountRepository.getAccount(s.accountId) ?: return@launch
|
|
val provider = runCatching { providerFactory.create(account) }.getOrElse { return@launch }
|
|
provider.createDirectory(newPath)
|
|
.onSuccess { retry() }
|
|
.onFailure { e -> _state.update { it.copy(error = "Could not create folder: ${e.message}") } }
|
|
}
|
|
}
|
|
|
|
fun retry() {
|
|
val s = _state.value
|
|
if (s.accountId == -1L) return
|
|
loadJob?.cancel()
|
|
_state.update { it.copy(isLoading = true, error = null) }
|
|
loadJob = loadPath(s.accountId, s.currentPath)
|
|
}
|
|
|
|
private fun loadPath(accountId: Long, path: String): Job = viewModelScope.launch {
|
|
val account = accountRepository.getAccount(accountId)
|
|
if (account == null) {
|
|
_state.update { it.copy(isLoading = false, error = "Account not found") }
|
|
return@launch
|
|
}
|
|
val provider = runCatching { providerFactory.create(account) }.getOrElse { e ->
|
|
_state.update { it.copy(isLoading = false, error = e.message ?: "Failed to create provider") }
|
|
return@launch
|
|
}
|
|
provider.listFiles(path)
|
|
.onSuccess { files ->
|
|
_state.update { it.copy(isLoading = false, entries = files.sortedWith(compareBy({ !it.isDirectory }, { it.name.lowercase() }))) }
|
|
}
|
|
.onFailure { e ->
|
|
_state.update { it.copy(isLoading = false, error = e.message ?: "Failed to list files") }
|
|
}
|
|
}
|
|
}
|