feat: add BreadcrumbBar for dir path
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
Reference in New Issue
Block a user