feat: add BreadcrumbBar for dir path

This commit is contained in:
Miuzarte
2026-07-08 19:14:00 +08:00
parent 8c8ccd896c
commit 0fa3a3d544
6 changed files with 91 additions and 42 deletions

View File

@@ -59,8 +59,8 @@ android {
applicationId = "io.github.miuzarte.scrcpyforandroid"
minSdk = 26
targetSdk = 37
versionCode = 33
versionName = "0.4.3_pre1"
versionCode = 34
versionName = "0.4.3"
externalNativeBuild {
cmake {

View File

@@ -59,7 +59,7 @@ fun FileManagerScreen(
val listState = rememberLazyListState()
val layoutDirection = LocalLayoutDirection.current
val pathStack by viewModel.pathStack.collectAsState()
val breadcrumbState by viewModel.breadcrumbState.collectAsState()
val currentPath by viewModel.currentPath.collectAsState()
val cachedEntries by viewModel.cachedEntries.collectAsState()
val loading by viewModel.loading.collectAsState()
@@ -117,7 +117,7 @@ fun FileManagerScreen(
if (pendingTreeDownload != null) treeLauncher.launch(null)
}
DisposableEffect(pathStack.size) {
DisposableEffect(canNavigateUp) {
onCanNavigateUpChange(canNavigateUp)
onNavigateUpActionChange?.invoke(viewModel::navigateUp)
onDispose {
@@ -144,7 +144,7 @@ fun FileManagerScreen(
haptic.contextClick()
viewModel.navigateUp()
},
enabled = pathStack.size > 1,
enabled = canNavigateUp,
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
@@ -153,30 +153,21 @@ fun FileManagerScreen(
}
},
bottomContent = {
Text(
text = currentPath,
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
haptic.contextClick()
pathInput = currentPath
showPathDialog = true
},
onLongClick = {
// 自带 haptic
pathInput = currentPath
showPathDialog = true
},
val breadcrumbItems = remember(breadcrumbState.stack) {
breadcrumbState.stack.map { path ->
BreadcrumbItem(
path = path,
text = path.substringAfterLast('/').ifEmpty { "/" },
)
.padding(
start = UiSpacing.PageHorizontal,
end = UiSpacing.PageHorizontal,
bottom = UiSpacing.Medium,
),
softWrap = false,
overflow = TextOverflow.Ellipsis,
color = colorScheme.onSurfaceVariantSummary,
}
}
BreadcrumbBar(
items = breadcrumbItems,
onItemClick = { index ->
haptic.contextClick()
viewModel.jumpToPath(breadcrumbState.stack[index])
},
highlightIndex = breadcrumbState.highlightIndex,
)
},
actions = {
@@ -241,6 +232,13 @@ fun FileManagerScreen(
OverlayIconDropdownMenu(
entry = DropdownEntry(
items = listOf(
DropdownItem(
text = stringResource(R.string.fm_goto_path),
onClick = {
pathInput = currentPath
showPathDialog = true
},
),
DropdownItem(
text = stringResource(R.string.fm_menu_create_folder),
onClick = {

View File

@@ -36,17 +36,57 @@ internal class FileManagerViewModel: ViewModel() {
)
private val asBundle = asBundleSync.value
private val _pathStack = MutableStateFlow(buildPathStack(INITIAL_REMOTE_PATH))
val pathStack: StateFlow<List<String>> = _pathStack.asStateFlow()
val currentPath: StateFlow<String> = _pathStack
.map { it.lastOrNull() ?: INITIAL_REMOTE_PATH }
private val _currentPath = MutableStateFlow(INITIAL_REMOTE_PATH)
val currentPath: StateFlow<String> = _currentPath.asStateFlow()
private val pathHistory = ArrayDeque<String>().apply { addLast(INITIAL_REMOTE_PATH) }
private val historyCapacity = 20
private fun addToHistory(path: String) {
pathHistory.remove(path)
pathHistory.addLast(path)
while (pathHistory.size > historyCapacity) pathHistory.removeFirst()
}
private fun trimHistory(path: String) {
pathHistory.retainAll { it == path || it.startsWith("$path/") || path.startsWith("$it/") }
}
private fun longestHistoricalPrefix(current: String): String {
val prefixMatch: (String) -> Boolean =
if (current == ROOT_REMOTE_PATH) { { true } }
else { { hist -> hist == current || hist.startsWith("$current/") } }
var best = current
var bestLen = current.length
pathHistory.forEach { hist ->
if (hist.length > bestLen && prefixMatch(hist)) {
best = hist
bestLen = hist.length
}
}
return best
}
internal data class BreadcrumbState(
val stack: List<String>,
val highlightIndex: Int,
)
val breadcrumbState: StateFlow<BreadcrumbState> = _currentPath
.map { current ->
val longest = longestHistoricalPrefix(current)
val stack = buildPathStack(longest)
val highlight = buildPathStack(current).lastIndex
BreadcrumbState(stack = stack, highlightIndex = highlight)
}
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
INITIAL_REMOTE_PATH,
BreadcrumbState(buildPathStack(INITIAL_REMOTE_PATH), buildPathStack(INITIAL_REMOTE_PATH).lastIndex),
)
val canNavigateUp: StateFlow<Boolean> = _pathStack
.map { it.size > 1 }
val canNavigateUp: StateFlow<Boolean> = _currentPath
.map { it != ROOT_REMOTE_PATH }
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
@@ -145,21 +185,31 @@ internal class FileManagerViewModel: ViewModel() {
}
fun navigateUp(): Boolean {
val stack = _pathStack.value
if (stack.size <= 1) return false
_pathStack.update { it.dropLast(1) }
val current = _currentPath.value
if (current == ROOT_REMOTE_PATH) return false
val parent = current.substringBeforeLast('/', ROOT_REMOTE_PATH)
_currentPath.value = parent
trimHistory(parent)
return true
}
fun jumpToPath(rawPath: String) {
val normalized = normalizePath(rawPath)
_pathStack.value = buildPathStack(normalized)
_currentPath.value = normalized
addToHistory(normalized)
trimHistory(normalized)
}
private fun pushPath(path: String) {
_currentPath.value = path
addToHistory(path)
trimHistory(path)
}
fun openEntry(entry: RemoteFileEntry) {
when {
entry.isDirectory -> {
_pathStack.update { it + normalizePath(entry.fullPath) }
pushPath(normalizePath(entry.fullPath))
}
entry.kind == RemoteFileKind.Link || entry.symlinkTarget != null -> {