diff --git a/EXPLORER.md b/EXPLORER.md index 0e8036e..4fac309 100644 --- a/EXPLORER.md +++ b/EXPLORER.md @@ -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 diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/FilesScreen.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/FilesScreen.kt index ba693e8..5a76caf 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/FilesScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/FilesScreen.kt @@ -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>() } 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(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 }, ) }