feat: add BreadcrumbBar for dir path
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- 改进: 对控制通道增加流控选项
|
- 改进: 对控制通道增加流控选项
|
||||||
- 新增: 预发行版更新检测
|
- 新增: 预发行版更新检测
|
||||||
|
- 改进: 文件页的路径改用新实现的面包屑
|
||||||
|
|
||||||
## 0.4.2
|
## 0.4.2
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 -> {
|
||||||
|
|||||||
Submodule submodule/miuix updated: b728c2d9ac...fd5362ce88
Reference in New Issue
Block a user