Restore directory navigation on Android back

This commit is contained in:
iris committed 2026-09-11 00:28:55 -04:00
1 parent 59965d314f
commit 22f263ccce
2 files changed
+45 -25

No files matched your search

+7 -6
View File
@@ -208,17 +208,18 @@ absence the signal. Back with unsaved changes asks, and says the edits will
be lost. The explorer draws over the session, which deliberately has no
`imePadding`, so the explorer's own box adds it.
### 10. The explorer draws over the session, and back closes it first
### 10. The explorer draws over the session, and the two back controls differ
`Screen.Session` in `AppRoot` gains a `files: FilesTarget?`. When set, the
`FilesScreen` is composed **on top of** the session in the same `Box`, and
the session stays composed under it: its event stream keeps flowing, its
scroll position and draft stay where they were, and returning from a file
costs nothing. Back — the button and the platform gesture — returns from an
open file to its containing directory, then clears `files` and returns to the
session. Directory navigation stays in the listing: `..` is an explicit row
rather than a hidden second meaning for Back. An editor with unsaved changes
asks before returning to the directory. "Back returns; it does not exit."
costs nothing. The header's back button clears `files` and returns directly to
the session. Android back returns from an open file to its containing directory,
then walks to the parent directory until the root; at the root it returns to the
session. The `..` row remains as the visible, tappable form of the same directory
movement. An editor with unsaved changes asks before either route discards them.
"Back returns; it does not exit."
Rejected: a `Screen.Files` beside `Screen.Session`. Every route back from a
leaf screen goes to Main today, and a session disposed and re-created on each
@@ -72,13 +72,18 @@ private sealed class Spot(val path: String) {
class Doc(path: String, val directory: Dir) : Spot(path)
}
private enum class UnsavedDestination {
Directory,
Session,
}
/**
* The files on the machine a session runs on: browse them, read one, change one.
*
* 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. Back from a file returns to its directory;
* back from a directory closes the explorer and returns to the session. Directory navigation stays
* inside the listing, where its `..` row is explicit.
* flowing and coming back from a file costs nothing. Android back returns from a file to its
* directory, then walks up the directory tree. The header's back button closes the explorer and
* returns directly to the session.
*
* 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
@@ -98,11 +103,10 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
val listings = remember { mutableStateMapOf<String, LoadState<Listing>>() }
var creating by remember { mutableStateOf(false) }
// Edit mode and whether anything has been typed live here rather than in the pane below,
// because they are what back has to know about -- and back arrives from two places, the arrow
// and the platform's own gesture, which must mean the same thing.
// because both ways out have to ask before discarding it.
var editing by remember { mutableStateOf(false) }
var dirty by remember { mutableStateOf(false) }
var askUnsaved by remember { mutableStateOf(false) }
var unsavedDestination by remember { mutableStateOf<UnsavedDestination?>(null) }
fun go(spot: Spot) {
editing = false
@@ -110,11 +114,23 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
here = spot
}
fun back() {
when {
editing && dirty -> askUnsaved = true
here is Spot.Doc -> go((here as Spot.Doc).directory)
else -> onClose()
fun leave(destination: UnsavedDestination) {
if (editing && dirty) {
unsavedDestination = destination
} else if (destination == UnsavedDestination.Directory) {
go((here as Spot.Doc).directory)
} else {
onClose()
}
}
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()
}
}
}
@@ -131,7 +147,7 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
}
}
BackHandler(onBack = ::back)
BackHandler(onBack = ::systemBack)
Box(
Modifier.fillMaxSize()
@@ -151,7 +167,7 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
title = baseName(at),
path = at,
machine = target.setupName,
onBack = ::back,
onBack = { leave(UnsavedDestination.Session) },
) {
GlyphButton(
REFRESH_GLYPH,
@@ -178,20 +194,23 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
editing = editing,
onEditing = { editing = it },
onDirty = { dirty = it },
onBack = ::back,
onBack = { leave(UnsavedDestination.Session) },
)
}
}
}
if (askUnsaved) {
unsavedDestination?.let { destination ->
UnsavedDialog(
onDiscard = {
askUnsaved = false
val spot = here
if (spot is Spot.Doc) go(spot.directory) else onClose()
unsavedDestination = null
if (destination == UnsavedDestination.Directory) {
go((here as Spot.Doc).directory)
} else {
onClose()
}
},
onCancel = { askUnsaved = false },
onCancel = { unsavedDestination = null },
)
}