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 be lost. The explorer draws over the session, which deliberately has no
`imePadding`, so the explorer's own box adds it. `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 `Screen.Session` in `AppRoot` gains a `files: FilesTarget?`. When set, the
`FilesScreen` is composed **on top of** the session in the same `Box`, and `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 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 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 costs nothing. The header's back button clears `files` and returns directly to
open file to its containing directory, then clears `files` and returns to the the session. Android back returns from an open file to its containing directory,
session. Directory navigation stays in the listing: `..` is an explicit row then walks to the parent directory until the root; at the root it returns to the
rather than a hidden second meaning for Back. An editor with unsaved changes session. The `..` row remains as the visible, tappable form of the same directory
asks before returning to the directory. "Back returns; it does not exit." 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 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 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) 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. * 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 * 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; * flowing and coming back from a file costs nothing. Android back returns from a file to its
* back from a directory closes the explorer and returns to the session. Directory navigation stays * directory, then walks up the directory tree. The header's back button closes the explorer and
* inside the listing, where its `..` row is explicit. * returns directly to the session.
* *
* Every directory that has been visited is kept for as long as this is open; the refresh glyph is * 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 * 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>>() } val listings = remember { mutableStateMapOf<String, LoadState<Listing>>() }
var creating by remember { mutableStateOf(false) } var creating by remember { mutableStateOf(false) }
// Edit mode and whether anything has been typed live here rather than in the pane below, // 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 // because both ways out have to ask before discarding it.
// and the platform's own gesture, which must mean the same thing.
var editing by remember { mutableStateOf(false) } var editing by remember { mutableStateOf(false) }
var dirty 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) { fun go(spot: Spot) {
editing = false editing = false
@@ -110,11 +114,23 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
here = spot here = spot
} }
fun back() { fun leave(destination: UnsavedDestination) {
when { if (editing && dirty) {
editing && dirty -> askUnsaved = true unsavedDestination = destination
here is Spot.Doc -> go((here as Spot.Doc).directory) } else if (destination == UnsavedDestination.Directory) {
else -> onClose() 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( Box(
Modifier.fillMaxSize() Modifier.fillMaxSize()
@@ -151,7 +167,7 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
title = baseName(at), title = baseName(at),
path = at, path = at,
machine = target.setupName, machine = target.setupName,
onBack = ::back, onBack = { leave(UnsavedDestination.Session) },
) { ) {
GlyphButton( GlyphButton(
REFRESH_GLYPH, REFRESH_GLYPH,
@@ -178,20 +194,23 @@ fun FilesScreen(settings: ServerSettings, target: FilesTarget, onClose: () -> Un
editing = editing, editing = editing,
onEditing = { editing = it }, onEditing = { editing = it },
onDirty = { dirty = it }, onDirty = { dirty = it },
onBack = ::back, onBack = { leave(UnsavedDestination.Session) },
) )
} }
} }
} }
if (askUnsaved) { unsavedDestination?.let { destination ->
UnsavedDialog( UnsavedDialog(
onDiscard = { onDiscard = {
askUnsaved = false unsavedDestination = null
val spot = here if (destination == UnsavedDestination.Directory) {
if (spot is Spot.Doc) go(spot.directory) else onClose() go((here as Spot.Doc).directory)
} else {
onClose()
}
}, },
onCancel = { askUnsaved = false }, onCancel = { unsavedDestination = null },
) )
} }