Plan waves 9-10: manual entry, then online search; chained runner
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWoivrRUEmJLFFwbsrGkqQ
This commit is contained in:
1 parent
0c0b8466cc
commit
9b74f50565
4 files changed
+605
No files matched your search
@@ -0,0 +1,162 @@
|
||||
You are implementing ONE feature in the Bookshelf Android app at ~/bookshelf.
|
||||
|
||||
READ FIRST, in this order:
|
||||
1. ~/bookshelf/docs/SPEC.md — the authoritative contract, especially "Non-negotiables",
|
||||
"Design language" and "Screens". Do not contradict it and do not edit it.
|
||||
2. The files named below, and their neighbours, before you write anything.
|
||||
|
||||
## The gap
|
||||
|
||||
There is currently NO way to add a book that has no barcode. Every path into
|
||||
"save a book" starts from an ISBN:
|
||||
- The scan screen's keyboard button (`ui/scan/ScanScreen.kt`, `showManualEntry`)
|
||||
only accepts an ISBN and then runs a metadata lookup on it.
|
||||
- `ManualEntrySheet` (ScanScreen.kt) only appears after a lookup comes back
|
||||
NotFound / LookupFailed, and `ScanViewModel.saveManualEntry(isbn13: String, ...)`
|
||||
REQUIRES an ISBN.
|
||||
Older books, pamphlets, self-published and foreign books often have no ISBN at all.
|
||||
The data layer already supports this: `BookRepository.createBook(isbn13: String? = null, ...)`
|
||||
takes every field as optional except title. This wave is UI + navigation work.
|
||||
|
||||
A follow-up wave (online search, runs right after you) will navigate into what you
|
||||
build here with a pre-filled title, and possibly other pre-filled fields. Design for
|
||||
that; do not build the search.
|
||||
|
||||
## What to build
|
||||
|
||||
### 1. An "Add a book by hand" screen
|
||||
|
||||
New package `org.modg.bookshelf.ui.add` — `AddBookScreen` + `AddBookViewModel`.
|
||||
A full screen (not a sheet: the library has no camera context, and this form is
|
||||
longer than the scan sheet's two fields). Fields, all optional except Title:
|
||||
Title (required), Subtitle, Authors (comma-separated, same convention as
|
||||
ManualEntrySheet), Publisher, Published (free text — SPEC's `publishedDate` is a
|
||||
text field; "1960" and "1999-04" are both legitimate), Pages (digits only),
|
||||
ISBN, Description, and a shelf picker.
|
||||
|
||||
- **Pre-fill.** Model the form as a plain data class (e.g. `BookDraft`) and let the
|
||||
ViewModel take an initial draft. Add a route in `ui/nav/Routes.kt` with OPTIONAL
|
||||
query arguments for at least `title` and `isbn`, e.g. `add?title={title}&isbn={isbn}`
|
||||
plus a helper `Routes.addBook(title: String? = null, isbn: String? = null)` that
|
||||
URL-encodes its arguments. (Titles contain `&`, `?`, `/`, `#` — test that the
|
||||
helper round-trips them.) The next wave may extend the draft; keep the draft →
|
||||
form mapping in one place so that is a small change.
|
||||
- **ISBN is optional but never silently discarded.** Blank = no ISBN. Non-blank must
|
||||
parse via `IsbnUtils.toIsbn13` (accepts ISBN-10 or -13, with or without hyphens);
|
||||
if it does not, mark the field in error with a message and disable Save. Wave 5
|
||||
fixed exactly this silent-discard bug in the manual-ISBN dialog — do not
|
||||
reintroduce it. Store the normalized ISBN-13; also store the ISBN-10 when the user
|
||||
typed a valid ISBN-10.
|
||||
- **Duplicate warning.** If the parsed ISBN-13 is already owned
|
||||
(`BookRepository.findByIsbn13`), show the same kind of warning the scan sheet
|
||||
shows (`DuplicateCheck` / `DuplicateStatus` in `ui/scan/ScanModels.kt` — reuse,
|
||||
do not duplicate). It is a warning, not a block: two copies of a book is legal.
|
||||
- **Shelf picker.** Reuse the existing shelf picker exactly as the scan sheet uses it
|
||||
(`ui/components/ShelfPickerSheet.kt` and the `ShelfPicker` call in ScanScreen.kt),
|
||||
including pre-selecting the remembered last shelf from `SettingsStore`, and
|
||||
remembering the chosen shelf on save the same way `ScanViewModel.rememberShelf` does.
|
||||
- **Two save actions**, because SPEC's real use case is shelving a box of books:
|
||||
"Save" — saves, then navigates to the new book's detail screen, removing the add
|
||||
screen from the back stack (so Back from detail returns to the library).
|
||||
"Save & add another" — saves, clears the form but KEEPS the shelf, shows a short
|
||||
confirmation naming the saved title, and a running count, and puts focus back in
|
||||
Title.
|
||||
- **Saving must not crash or lose the form.** Wave 8 made the lookup path
|
||||
exception-safe but flagged that SAVE paths still make unguarded Room calls. Guard
|
||||
yours: catch `Throwable` around the save, RETHROW `CancellationException` first
|
||||
(`kotlinx.coroutines.CancellationException`), and on failure keep the typed form
|
||||
and show an error naming the exception class (not its message). Disable the save
|
||||
buttons while a save is in flight so a double-tap cannot create two books.
|
||||
- Offline-first (SPEC): Room only. Nothing on this screen touches the network.
|
||||
- IME: Next between fields, sensible keyboard types (Number for pages, capitalize
|
||||
words for title/authors), and make sure the focused field is not hidden behind
|
||||
the keyboard — the setup screen hit this; see how it was fixed there
|
||||
(`safeDrawingPadding` outside `verticalScroll`, `adjustResize` is already set).
|
||||
- **Split a stateless `AddBookContent(state, callbacks)` out of the screen** so the
|
||||
Paparazzi snapshot renders the REAL composable. Wave 6 shipped a hand-rolled
|
||||
lookalike snapshot for Locations and it is recorded in HANDOFF.md as a known
|
||||
soft spot; do not repeat that.
|
||||
|
||||
### 2. Entry points
|
||||
|
||||
- **Library screen** (`ui/library/LibraryScreen.kt`): add an entry point next to the
|
||||
scan FAB. Use a Material 3 pattern that keeps Scan as the primary action — e.g. a
|
||||
`SmallFloatingActionButton` ("Add by hand", edit/pencil-style icon) stacked above
|
||||
the main FAB. Pick what looks right against SPEC's design language; justify it in
|
||||
your report. Also give the empty state a secondary "Add by hand" action next to
|
||||
"Scan a book". New `LibraryScreen` parameter, wired in `BookshelfNavHost`.
|
||||
- **Scan screen**: the manual-ISBN dialog (the keyboard button) gets a clear
|
||||
secondary action like "No ISBN? Enter the details by hand" that navigates to the
|
||||
add screen. New `ScanScreen` callback parameter, wired in `BookshelfNavHost`.
|
||||
- Leave the in-scan `ManualEntrySheet` (post-lookup NotFound/LookupFailed) as it is —
|
||||
it keeps the scanned ISBN and continuous-scan flow. You may give it a
|
||||
"More fields…" action that opens the add screen pre-filled with that ISBN and the
|
||||
title/authors typed so far, if it is small; otherwise leave it and say so.
|
||||
|
||||
## Constraints — these are hard
|
||||
|
||||
- Kotlin, Compose, Material 3. Match surrounding code's style, naming and comment
|
||||
density (this codebase comments the evidence for a decision, not a restatement of
|
||||
the code). Read neighbouring files first.
|
||||
- You MAY touch: new `ui/add/`, `ui/nav/`, `ui/library/`, `ui/scan/` (entry point
|
||||
and optional "More fields…" only), `ui/components/` (only if a shared piece must
|
||||
move there to be reused), and tests. Nothing else.
|
||||
- DO NOT touch: any build file (`app/app/build.gradle.kts`, `gradle/libs.versions.toml`,
|
||||
etc.), `data/**`, `server/`, `docs/`. If you believe you need a dependency or a
|
||||
data-layer change, STOP and say so in your report. Everything above is doable with
|
||||
what exists (`createBook` already takes all these fields).
|
||||
- DO NOT read, print or grep `app/local.properties`.
|
||||
- DO NOT git commit, add or push. Leave the work in the working tree.
|
||||
- Build with `./tasks/gw <task>` — NEVER `./gradlew` directly (flock-serialized wrapper).
|
||||
- RUN BUILDS IN THE FOREGROUND. Never background a Gradle build and end your turn
|
||||
saying you will report later — you will never get to. Several previous workers on
|
||||
this project did exactly that. A build takes 1-5 minutes; wait for it.
|
||||
- No emulator on this box. You cannot run the app. Do not claim you did.
|
||||
|
||||
## Definition of done
|
||||
|
||||
Run these yourself, in the foreground, and paste the real output tails:
|
||||
|
||||
./tasks/gw assembleDebug -> exit 0
|
||||
./tasks/gw testDebugUnitTest -> exit 0. **205 tests today, 2 skipped** (both
|
||||
opt-in live tests). Count must go UP; nothing
|
||||
may regress. Sum the TEST-*.xml files
|
||||
(app/app/build/test-results/testDebugUnitTest/)
|
||||
— do not read the console.
|
||||
./tasks/gw recordPaparazziDebug -> exit 0
|
||||
./tasks/gw verifyPaparazziDebug -> exit 0 against the re-recorded snapshots
|
||||
|
||||
Required tests with REAL assertions (assertion-free tests violate SPEC):
|
||||
- `Routes.addBook` round-trips titles containing `&`, `?`, `/`, `#`, spaces and
|
||||
non-ASCII (e.g. "Hänsel & Gretel / Part 1?"), and omits absent args.
|
||||
- Draft validation: blank title disables save; blank ISBN is valid (saves null);
|
||||
ISBN-10 is normalized to ISBN-13 and the ISBN-10 kept; a bad checksum is an
|
||||
error, not a silent drop; non-digit pages rejected.
|
||||
- ViewModel: save calls `createBook` with every field mapped (authors split and
|
||||
trimmed, empty entries dropped); duplicate ISBN produces the AlreadyOwned
|
||||
warning; "save & add another" clears fields but keeps the shelf and increments
|
||||
the count; a repository that throws leaves the form intact with an error and
|
||||
does not throw; a `CancellationException` is NOT swallowed; a second save while
|
||||
one is in flight does not create a second book.
|
||||
- Paparazzi (light + dark, in `ui/screens/` alongside the others): the empty form,
|
||||
a filled form with a duplicate-ISBN warning, and the ISBN-error state. Plus the
|
||||
library screen with the new FAB, re-recorded.
|
||||
|
||||
Also grep the build output for Kotlin warnings on files you touched. "Check for
|
||||
instance is always 'false'" is NOT cosmetic — it hid a bug that blanked every book
|
||||
cover in this app for months.
|
||||
|
||||
## Report
|
||||
|
||||
End with a plain report:
|
||||
- what you changed, file by file
|
||||
- verbatim tails of each gradle command above
|
||||
- test count before and after, summed from the XML
|
||||
- the exact route string and `Routes.addBook` signature, and how `BookDraft` is
|
||||
passed in — the next wave's worker will read this report to build on it
|
||||
- judgement calls you made (FAB pattern, "More fields…" yes/no) and why
|
||||
- anything you could NOT do or did differently, and why
|
||||
- anything you noticed that looks wrong but was out of scope
|
||||
|
||||
Be honest. Workers here have over-claimed before, and everything is re-verified, so
|
||||
an inflated report only wastes a round trip.
|
||||
Reference in new issue
Block a user