perf: optimize screen load times with progressive un-gating and SWR caching (#1192)

This commit is contained in:
M57 authored and GitHub committed 2026-09-18 23:25:30 +03:00
1 parent abce60ad7a
commit f5eaecbbef
55 files changed
+2494 -401

No files matched your search

@@ -45,6 +45,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -57,6 +58,7 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.model.ManagedFileEntry
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
@@ -79,6 +81,7 @@ fun FilesScreen(
viewModel: FilesViewModel = viewModel { FilesViewModel() },
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val dataScope by AuthManager.dataScopeFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
val uploadLauncher =
@@ -104,6 +107,13 @@ fun FilesScreen(
onDispose { viewModel.clearTransientState() }
}
// Scope-aware load: a new server/profile context must never keep the old
// server's directory path or entries on screen (PR #1192 cache scoping).
LaunchedEffect(dataScope) {
viewModel.clearScopeOwnedState()
viewModel.load()
}
val hasEntries = state.entries.isNotEmpty()
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
@@ -3,15 +3,18 @@ package com.m57.hermescontrol.ui.files
import android.util.Base64
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.data.local.SwrCache
import com.m57.hermescontrol.data.model.ManagedDirectoryCreate
import com.m57.hermescontrol.data.model.ManagedFileActionResponse
import com.m57.hermescontrol.data.model.ManagedFileDelete
import com.m57.hermescontrol.data.model.ManagedFileEntry
import com.m57.hermescontrol.data.model.ManagedFilesListResponse
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.ui.common.ToastHost
import com.m57.hermescontrol.ui.common.safeLaunchLoad
import com.m57.hermescontrol.ui.common.safeLaunchSwrLoad
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -76,8 +79,52 @@ class FilesViewModel :
private val _uiState = MutableStateFlow(FilesUiState())
val uiState: StateFlow<FilesUiState> = _uiState.asStateFlow()
fun load(path: String? = null) {
safeLaunchLoad(
private val filesCache = SwrCache<String, ManagedFilesListResponse>()
/**
* Reset state owned by the previous server/profile context (issue: PR #1192 cache scoping).
* The current directory path belongs to the old server and must not leak into the new one.
*/
fun clearScopeOwnedState() {
_uiState.update {
it.copy(
currentPath = "",
parentPath = null,
crumbs = emptyList(),
entries = emptyList(),
isLoading = false,
errorMessage = null,
busyPaths = emptySet(),
isCreatingDir = false,
newDirName = "",
isUploading = false,
deleteTarget = null,
openingPath = null,
)
}
}
fun load(
path: String? = null,
forceRefresh: Boolean = false,
) {
val cacheKey = path ?: ""
safeLaunchSwrLoad(
cache = filesCache,
cacheKey = cacheKey,
forceRefresh = forceRefresh,
onCacheHit = { data ->
_uiState.update { state ->
state.copy(
isLoading = false,
currentPath = data.path,
parentPath = data.parent,
crumbs = buildCrumbs(data.path),
entries = data.entries,
errorMessage = null,
)
}
},
apiCall = {
safeApiCall { ApiClient.hermesApi.listManagedFiles(path) }
},
@@ -104,7 +151,7 @@ class FilesViewModel :
)
}
fun refresh() = load(_uiState.value.currentPath.ifBlank { null })
fun refresh() = load(_uiState.value.currentPath.ifBlank { null }, forceRefresh = true)
fun navigateTo(entry: ManagedFileEntry) {
if (!entry.isDirectory) return