From 0fa3a3d54437eaa9f5a746aee977b7c629eb195a Mon Sep 17 00:00:00 2001
From: Miuzarte <982809597@qq.com>
Date: Wed, 8 Jul 2026 19:14:00 +0800
Subject: [PATCH] feat: add BreadcrumbBar for dir path
---
CHANGELOG.md | 1 +
README.md | 2 +-
app/build.gradle.kts | 4 +-
.../pages/FileManagerScreen.kt | 50 ++++++-------
.../pages/FileManagerViewModel.kt | 74 ++++++++++++++++---
submodule/miuix | 2 +-
6 files changed, 91 insertions(+), 42 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d5b9dc..6df5e25 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
- 改进: 对控制通道增加流控选项
- 新增: 预发行版更新检测
+- 改进: 文件页的路径改用新实现的面包屑
## 0.4.2
diff --git a/README.md b/README.md
index e61405c..0f3ad72 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@
-
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index da2b97b..b34b8c2 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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 {
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerScreen.kt
index e2eb384..f2dfe2e 100644
--- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerScreen.kt
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerScreen.kt
@@ -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 = {
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerViewModel.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerViewModel.kt
index 7a831ef..5fa7b84 100644
--- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerViewModel.kt
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerViewModel.kt
@@ -36,17 +36,57 @@ internal class FileManagerViewModel: ViewModel() {
)
private val asBundle = asBundleSync.value
- private val _pathStack = MutableStateFlow(buildPathStack(INITIAL_REMOTE_PATH))
- val pathStack: StateFlow> = _pathStack.asStateFlow()
- val currentPath: StateFlow = _pathStack
- .map { it.lastOrNull() ?: INITIAL_REMOTE_PATH }
+ private val _currentPath = MutableStateFlow(INITIAL_REMOTE_PATH)
+ val currentPath: StateFlow = _currentPath.asStateFlow()
+
+ private val pathHistory = ArrayDeque().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,
+ val highlightIndex: Int,
+ )
+
+ val breadcrumbState: StateFlow = _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 = _pathStack
- .map { it.size > 1 }
+
+ val canNavigateUp: StateFlow = _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 -> {
diff --git a/submodule/miuix b/submodule/miuix
index b728c2d..fd5362c 160000
--- a/submodule/miuix
+++ b/submodule/miuix
@@ -1 +1 @@
-Subproject commit b728c2d9acf9578fc92d1e023334abf95f95e1aa
+Subproject commit fd5362ce88fa0b75b2df1827ec395bfd29fa9d28