Copy to… and Move to… chose a destination instead of making you go there

The selection bar's overflow offers both verbs. Confirming opens a folder picker - the volumes first, then folders one level at a time, with the trail above and Up beside it - and starts the copy or the move into the picked folder without leaving the one you are in. The files stay selected after a copy, so sending the same set to a second destination is two taps; a move clears them because the rows are gone.

Underneath it is the paste path with an explicit target: planTransfer and transfer were always target-agnostic, so the new code is the picker and the rules around it. A conflict asks exactly what a paste asks and the prompt carries the picked folder.

One thing needed care: the reload that follows every operation clears the selection, and it finishes after the operation returns because it is a coroutine of its own. The restore therefore happens at the end of that reload, through a field the transfer sets - otherwise the last writer wins and a copy silently loses the selection it promised to keep, which is exactly what the first version did until the test caught it.

Seven view-model tests (copy, move, folders-only listing, the walk up and down, the conflict target, cancellation, the selection rule) and one Compose test for the overflow menu. CHANGELOG and ROADMAP record it, the roadmap naming what is deliberately not done: the copied-to-X message with a jump to the destination.
This commit is contained in:
2026-10-17 22:31:28 +02:00
parent 2a40302982
commit f3abef6c44
11 changed files with 487 additions and 11 deletions
+15 -8
View File
@@ -2,7 +2,7 @@
Generated from the source by `tools/make-code-map.py` - do not edit by hand. Regenerate it after a change (`python3 tools/make-code-map.py`); `--check` exits non-zero when this file is stale.
79 source files, 13040 lines - 35 test files, 396 tests. The behaviour index at the end lists every test name, and in this project a test name is a sentence about what the app does, so it doubles as the specification.
79 source files, 13369 lines - 35 test files, 403 tests. The behaviour index at the end lists every test name, and in this project a test name is a sentence about what the app does, so it doubles as the specification.
Start at `AGENTS.md`. What is done, and why: `docs/ROADMAP.md` and `CHANGELOG.md`. What was verified by hand rather than by a test: `docs/EDGE_CASE_TESTING.md`.
@@ -98,17 +98,17 @@ The browser screen: view model, state, list and grid, dialogs, selection bar
| `BrowserComponents.kt` | 317 | The thumbnail an entry's leading visual gets in a list row. ListThumbnailSize, GridThumbnailSize, GridCellMinSize, EntryIcon(), FileEntry(), FileEntry(), VIDEO_FRAME_MILLIS, iconFor(), iconFor(), EntryRow(), EntryGridCell(), FileEntry() | - |
| `BrowserContent.kt` | 463 | Everything below the app bar: the permission prompt, the roots, or a directory. WIDE_LAYOUT_MIN_WIDTH, MasterPaneWidth, BrowserContent(), LazyListScope(), LIBRARY_SECTION_KEY, LazyListScope(), BOOKMARK_SECTION_KEY, DirectoryView(), FileEntry(), IndexStatusRow(), FileEntry() | - |
| `BrowserDetailPane.kt` | 131 | The second pane of the wide layout: what is inside the folder the user picked, or a preview and the details of the file they picked. DetailPane(), FolderPreview(), ImagePreview() | - |
| `BrowserDialogs.kt` | 279 | Renaming several files at once, with the numbering worked out before anything moves. BatchRenameDialog(), PREVIEW_NAMES, batchRenameNote(), NameDialog(), ConfirmDialog(), ConflictDialog() | `BrowserDialogsTest` (5) |
| `BrowserDialogs.kt` | 381 | The Copy to… / Move to… picker. TransferToDialog(), titleFor(), hereFor(), BatchRenameDialog(), PREVIEW_NAMES, batchRenameNote(), NameDialog(), ConfirmDialog(), ConflictDialog() | `BrowserDialogsTest` (5) |
| `BrowserRoots.kt` | 270 | A row on the root screen: a volume, with its free space when the platform reports it. RootRow(), itemCountLabel(), EmptyState(), LibrarySectionHeader(), BookmarkSectionHeader(), SectionHeader(), BookmarkRow(), LibraryRow(), Breadcrumbs() | `BrowserRootsTest` (4) |
| `BrowserScreen.kt` | 591 | BrowserScreen() | - |
| `BrowserScreen.kt` | 604 | BrowserScreen() | - |
| `BrowserTopBar.kt` | 238 | The browser's app bar: title, up, the favourite star, and the search / sort / overflow actions. BrowserTopBar(), SEARCH_FIELD_WIDTH | `BrowserTopBarTest` (4) |
| `BrowserUiState.kt` | 311 | One immutable description of everything the browser shows. ViewMode, SearchScope, Clipboard, UiMessage, ConflictPrompt, RootItem, Bookmark, DetailFocus, Undo, UndoMove, Overlay, BrowserUiState | - |
| `BrowserViewModel.kt` | 2237 | Drives the browser screen. BrowserViewModel, ArchivePreview | `BrowserViewModelTest` (90) |
| `BrowserUiState.kt` | 343 | One immutable description of everything the browser shows. ViewMode, SearchScope, Clipboard, PickerFolder, TransferPicker, UiMessage, ConflictPrompt, RootItem, Bookmark, DetailFocus, Undo, UndoMove, Overlay, BrowserUiState | - |
| `BrowserViewModel.kt` | 2376 | Drives the browser screen. BrowserViewModel, ArchivePreview | `BrowserViewModelTest` (96) |
| `DetailsSheet.kt` | 109 | Name, location, size, type and modified date for one entry. DetailsSheet(), DetailLines(), DetailLine() | - |
| `EntryIntents.kt` | 58 | Wraps an entry's path in a content:// URI another app may read. contentUriFor(), viewIntent(), shareIntent() | - |
| `IncomingBar.kt` | 60 | The bar shown while another app is waiting on this one. IncomingBar() | - |
| `IncomingRequest.kt` | 46 | What another app asked this one to do. IncomingRequest | - |
| `SelectionBar.kt` | 176 | The contextual action bar shown while entries are selected. StorageAccessPrompt(), SelectionBar() | `SelectionBarTest` (2) |
| `SelectionBar.kt` | 219 | The contextual action bar shown while entries are selected. StorageAccessPrompt(), SelectionBar() | `SelectionBarTest` (3) |
| `SortSheet.kt` | 128 | Sort key, direction, hidden files and list/grid — all in one sheet. sortKeyLabel(), SortSheet() | - |
## ui/viewer/
@@ -581,7 +581,7 @@ Every test name in the suite, by file: the fastest way to see whether something
- a library or the root list has nothing to star
- the star gives way to the search field
### `ui/browser/BrowserViewModelTest.kt` (90)
### `ui/browser/BrowserViewModelTest.kt` (96)
- starts on the root list
- opening a folder lists it with directories first and natural order
@@ -589,6 +589,12 @@ Every test name in the suite, by file: the fastest way to see whether something
- search narrows the listing
- sorting by size reorders the listing
- a folder of six hundred files is still listed in natural order
- copy to a chosen folder leaves the user where they are and keeps the selection
- move to a chosen folder takes the rows with it
- the picker offers folders and nothing else
- the picker walks up to the volumes and back down
- a conflicting destination asks before overwriting, naming that folder
- cancelling the picker copies nothing
- navigating up goes to the parent then to the roots
- selection survives toggling and clears on navigation
- a file with no preview of its own is handed to the system
@@ -674,8 +680,9 @@ Every test name in the suite, by file: the fastest way to see whether something
- while providing, tapping a file chooses it instead of opening it
- while providing, a folder still navigates so the file can actually be reached
### `ui/browser/SelectionBarTest.kt` (2)
### `ui/browser/SelectionBarTest.kt` (3)
- the destination verbs live in the overflow menu
- several files are offered a rename and not the details
- a single file keeps both
+9 -3
View File
@@ -1,8 +1,8 @@
# Roadmap
File Manager is built against a fixed milestone plan (**M0–M15**). This file is the
single source of truth for what is done and what remains. M16 is specified below but **not scheduled** —
it is the first item of the post-1.0 queue that is a feature rather than a safeguard.
single source of truth for what is done and what remains. **M16** (Copy to… / Move to…) has since been
built and is specified below; the queue records what is left.
Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (proposed, not scheduled)
@@ -81,7 +81,13 @@ release should not go out without.
still unmeasured, and jank on this screen needs a device that reports frame durations.
3. **The scaling numbers** — `./tools/benchmark-fixture.sh --count 20000` and the existing benchmarks:
answers whether a full SD card is a performance problem before a user with one finds out.
4. **Copy to… / Move to… (M16)** — the comfort feature, specified below.
4. **Copy to… / Move to… (M16)** — **done**: the selection bar's overflow opens a folder picker (volumes,
then folders one level at a time, Up and the trail above them) and confirming copies or moves into the
picked folder without leaving the current one. The selection survives a copy so a second destination is
two taps; a move clears it. Conflicts ask the same question as a paste, naming the picked folder. Seven
tests cover the verbs, the picker's walk, folders-only listing, the conflict target, cancellation, and
the selection rule. **Not done**: the "copied to X — Show" message with a jump to the destination; the
copy reports itself the way a paste does.
5. **A CI run of the benchmarks on an emulator** — removes the phone from the loop and reports
`frameDurationCpuMs`, which this device cannot.