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 @@ Streaming Scrcpy All Options Terminal - Files + Files Settings Multi Touch Test Virtual Buttons Reorder 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