Navigate explorer back toward project
This commit is contained in:
1 parent
2ff0b13950
commit
62cb6c91d5
3 files changed
+119
-26
No files matched your search
@@ -82,8 +82,8 @@ private enum class UnsavedDestination {
|
||||
*
|
||||
* Drawn **over** the session rather than instead of it (see [AppRoot]), so its event stream keeps
|
||||
* flowing and coming back from a file costs nothing. Both back controls return from a file to its
|
||||
* directory. In a directory, Android back walks up the directory tree while the header's back
|
||||
* button closes the explorer and returns to the session.
|
||||
* directory. In a directory, Android back walks toward the session's project directory and closes
|
||||
* the explorer once it gets there; the header's back button closes it immediately.
|
||||
*
|
||||
* Every directory that has been visited is kept for as long as this is open; the refresh glyph is
|
||||
* how one gets asked again on purpose, and creating something refetches the directory it was
|
||||
@@ -124,18 +124,9 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
|
||||
}
|
||||
}
|
||||
|
||||
fun systemBack() {
|
||||
when (val spot = here) {
|
||||
is Spot.Doc -> leave(UnsavedDestination.Directory)
|
||||
is Spot.Dir -> {
|
||||
val path = (listings[spot.path] as? LoadState.Loaded)?.value?.path ?: spot.path
|
||||
parentOf(path)?.let { go(Spot.Dir(it)) } ?: onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun load(path: String, again: Boolean) {
|
||||
if (!again && listings[path] is LoadState.Loaded) return
|
||||
val existing = listings[path]
|
||||
if (!again && (existing is LoadState.Loaded || existing is LoadState.Loading)) return
|
||||
listings[path] = LoadState.Loading
|
||||
listings[path] =
|
||||
try {
|
||||
@@ -147,6 +138,35 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
|
||||
}
|
||||
}
|
||||
|
||||
val projectDirectory = (listings[target.start] as? LoadState.Loaded)?.value?.path
|
||||
val homeDirectory =
|
||||
if (target.start == "~") projectDirectory
|
||||
else (listings["~"] as? LoadState.Loaded)?.value?.path
|
||||
|
||||
fun systemBack() {
|
||||
when (val spot = here) {
|
||||
is Spot.Doc -> leave(UnsavedDestination.Directory)
|
||||
is Spot.Dir -> {
|
||||
val path = (listings[spot.path] as? LoadState.Loaded)?.value?.path ?: spot.path
|
||||
when {
|
||||
path == projectDirectory || path == target.start -> onClose()
|
||||
projectDirectory != null ->
|
||||
nextDirectoryToward(path, projectDirectory)?.let { go(Spot.Dir(it)) }
|
||||
?: onClose()
|
||||
else -> parentOf(path)?.let { go(Spot.Dir(it)) } ?: onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A file link can open without visiting the project first, but Back still needs to know where
|
||||
// the project is. Home is likewise resolved by the machine rather than guessed on the phone;
|
||||
// it is what lets every path beneath it be displayed with `~`, including over ssh.
|
||||
LaunchedEffect(target.setup, target.start) {
|
||||
if (target.file != null) load(target.start, again = false)
|
||||
if (target.start != "~") load("~", again = false)
|
||||
}
|
||||
|
||||
BackHandler(onBack = ::systemBack)
|
||||
|
||||
Box(
|
||||
@@ -160,12 +180,13 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
|
||||
when (val spot = here) {
|
||||
is Spot.Dir -> {
|
||||
val state = listings[spot.path] ?: LoadState.Loading
|
||||
// The resolved path once there is one: a directory opened as `~` is called what
|
||||
// it turned out to be, not what it was asked for.
|
||||
// Navigate with the resolved path, but name anything under the machine's home
|
||||
// the way somebody working there would write it.
|
||||
val at = (state as? LoadState.Loaded)?.value?.path ?: spot.path
|
||||
val shownAt = tildePath(at, homeDirectory)
|
||||
FilesHeader(
|
||||
title = baseName(at),
|
||||
path = at,
|
||||
title = baseName(shownAt),
|
||||
path = shownAt,
|
||||
machine = target.setupName,
|
||||
onBack = { leave(UnsavedDestination.Session) },
|
||||
) {
|
||||
@@ -192,6 +213,7 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
|
||||
path = spot.path,
|
||||
name = baseName(spot.path),
|
||||
editing = editing,
|
||||
homeDirectory = homeDirectory,
|
||||
onEditing = { editing = it },
|
||||
onDirty = { dirty = it },
|
||||
onBack = { leave(UnsavedDestination.Directory) },
|
||||
@@ -397,6 +419,7 @@ private fun ColumnScope.DocPane(
|
||||
path: String,
|
||||
name: String,
|
||||
editing: Boolean,
|
||||
homeDirectory: String?,
|
||||
onEditing: (Boolean) -> Unit,
|
||||
onDirty: (Boolean) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
@@ -470,7 +493,12 @@ private fun ColumnScope.DocPane(
|
||||
}
|
||||
}
|
||||
|
||||
FilesHeader(title = name, path = path, machine = target.setupName, onBack = onBack) {
|
||||
FilesHeader(
|
||||
title = name,
|
||||
path = tildePath(path, homeDirectory),
|
||||
machine = target.setupName,
|
||||
onBack = onBack,
|
||||
) {
|
||||
if (editing) {
|
||||
if (saving) {
|
||||
GlyphSpinner("Saving")
|
||||
@@ -718,6 +746,35 @@ internal fun parentOf(path: String): String? {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The next directory on the filesystem path from [current] to [destination], or null when there.
|
||||
*
|
||||
* Moving between two branches first walks upward to their common ancestor. Once [current] is that
|
||||
* ancestor, the next press walks one segment down toward [destination]. Both paths are answers from
|
||||
* the machine, so they are absolute and have no symlinks or `..` left to resolve here.
|
||||
*/
|
||||
internal fun nextDirectoryToward(current: String, destination: String): String? {
|
||||
val here = current.trimEnd('/').ifEmpty { "/" }
|
||||
val there = destination.trimEnd('/').ifEmpty { "/" }
|
||||
if (here == there) return null
|
||||
val beneathHere = if (here == "/") there.startsWith('/') else there.startsWith("$here/")
|
||||
if (!beneathHere) return parentOf(here)
|
||||
val next = there.removePrefix(here).trimStart('/').substringBefore('/')
|
||||
return join(here, next)
|
||||
}
|
||||
|
||||
/** A path as somebody on [home] writes it, leaving paths outside that home unchanged. */
|
||||
internal fun tildePath(path: String, home: String?): String {
|
||||
val at = path.trimEnd('/').ifEmpty { "/" }
|
||||
val resolvedHome = home?.trimEnd('/')?.ifEmpty { "/" } ?: return at
|
||||
return when {
|
||||
at == resolvedHome -> "~"
|
||||
resolvedHome != "/" && at.startsWith("$resolvedHome/") ->
|
||||
"~${at.removePrefix(resolvedHome)}"
|
||||
else -> at
|
||||
}
|
||||
}
|
||||
|
||||
/** What a path names: its last segment, with `/` naming itself. */
|
||||
internal fun baseName(path: String): String {
|
||||
val trimmed = path.trimEnd('/')
|
||||
|
||||
Reference in new issue
Block a user