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

@@ -4,6 +4,7 @@
- 改进: 对控制通道增加流控选项 - 改进: 对控制通道增加流控选项
- 新增: 预发行版更新检测 - 新增: 预发行版更新检测
- 改进: 文件页的路径改用新实现的面包屑
## 0.4.2 ## 0.4.2

View File

@@ -29,7 +29,7 @@
<img src="https://github.com/user-attachments/assets/f1271cc7-ce45-46d2-8a01-0f4a367c0e4a" height="300" alt="Streaming" /> <img src="https://github.com/user-attachments/assets/f1271cc7-ce45-46d2-8a01-0f4a367c0e4a" height="300" alt="Streaming" />
<img src="https://github.com/user-attachments/assets/220fc973-f3b7-4bec-8733-95bbddf95a7a" height="300" alt="Scrcpy All Options" /> <img src="https://github.com/user-attachments/assets/220fc973-f3b7-4bec-8733-95bbddf95a7a" height="300" alt="Scrcpy All Options" />
<img src="https://github.com/user-attachments/assets/6d4cff1b-1277-44e4-bcfa-18201738a703" height="300" alt="Terminal" /> <img src="https://github.com/user-attachments/assets/6d4cff1b-1277-44e4-bcfa-18201738a703" height="300" alt="Terminal" />
<img src="https://github.com/user-attachments/assets/886ba6db-d6a7-4528-96fb-bb0ce4deb953" height="300" alt="Files" /> <img src="https://github.com/user-attachments/assets/3e4edc6c-ddfc-478b-aa7d-aa797a9f0e74" height="300" alt="Files" />
<img src="https://github.com/user-attachments/assets/d3b29861-6e86-4301-9b1d-1f7836ad9b7e" height="300" alt="Settings" /> <img src="https://github.com/user-attachments/assets/d3b29861-6e86-4301-9b1d-1f7836ad9b7e" height="300" alt="Settings" />
<img src="https://github.com/user-attachments/assets/f513b7ba-0389-4176-8382-c1a08c4eba99" height="300" alt="Multi Touch Test" /> <img src="https://github.com/user-attachments/assets/f513b7ba-0389-4176-8382-c1a08c4eba99" height="300" alt="Multi Touch Test" />
<img src="https://github.com/user-attachments/assets/7a50bd1f-8095-4269-8e58-88316d86e3d8" height="300" alt="Virtual Buttons Reorder" /> <img src="https://github.com/user-attachments/assets/7a50bd1f-8095-4269-8e58-88316d86e3d8" height="300" alt="Virtual Buttons Reorder" />

View File

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

View File

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

View File

@@ -36,17 +36,57 @@ internal class FileManagerViewModel: ViewModel() {
) )
private val asBundle = asBundleSync.value private val asBundle = asBundleSync.value
private val _pathStack = MutableStateFlow(buildPathStack(INITIAL_REMOTE_PATH)) private val _currentPath = MutableStateFlow(INITIAL_REMOTE_PATH)
val pathStack: StateFlow<List<String>> = _pathStack.asStateFlow() val currentPath: StateFlow<String> = _currentPath.asStateFlow()
val currentPath: StateFlow<String> = _pathStack
.map { it.lastOrNull() ?: INITIAL_REMOTE_PATH } 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( .stateIn(
viewModelScope, viewModelScope,
SharingStarted.Eagerly, 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( .stateIn(
viewModelScope, viewModelScope,
SharingStarted.Eagerly, SharingStarted.Eagerly,
@@ -145,21 +185,31 @@ internal class FileManagerViewModel: ViewModel() {
} }
fun navigateUp(): Boolean { fun navigateUp(): Boolean {
val stack = _pathStack.value val current = _currentPath.value
if (stack.size <= 1) return false if (current == ROOT_REMOTE_PATH) return false
_pathStack.update { it.dropLast(1) } val parent = current.substringBeforeLast('/', ROOT_REMOTE_PATH)
_currentPath.value = parent
trimHistory(parent)
return true return true
} }
fun jumpToPath(rawPath: String) { fun jumpToPath(rawPath: String) {
val normalized = normalizePath(rawPath) 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) { fun openEntry(entry: RemoteFileEntry) {
when { when {
entry.isDirectory -> { entry.isDirectory -> {
_pathStack.update { it + normalizePath(entry.fullPath) } pushPath(normalizePath(entry.fullPath))
} }
entry.kind == RemoteFileKind.Link || entry.symlinkTarget != null -> { entry.kind == RemoteFileKind.Link || entry.symlinkTarget != null -> {