Keep Only Pinned — Design¶
Date: 2026-06-18 Status: Approved (design) Scope: A bulk "keep only pinned" action that prunes the 24-slot store down to just the pinned presets, compacted to the front. Follow-up to the preset-slots feature (T9–T11). Engine untouched.
Motivation¶
After the slot/iCloud sign-off, the user wants to declutter to just the presets they pinned — "keep only the ones I pinned." Today the only bulk action is Reset all → factory; clearing the ~20 non-pinned slots by hand (per-slot Clear context menu) is tedious and non-repeatable.
Behavior¶
Keep only pinned (compacting):
1. Collect the pinned filled presets in slot order (this is rotation when pins are non-empty),
preserving each slot's userOwned flag.
2. Empty all 24 slots; refill slot-00 … slot-(k-1) with the kept presets (each preset's id is
rewritten to its new slot id, as fill always does); higher slots stay empty.
3. Pins are preserved, remapped to the new slot ids. So all kept slots are pinned → the ATV
rotation (all pinned → all filled) is exactly the kept set. The user can curate a narrower ATV
subset afterward by un-pinning (un-pinning never deletes).
4. No-op when nothing is pinned (guarded in the model AND the button is disabled) — otherwise it
would wipe every slot.
Selection: the slot ids shift, so the prior selectedID is stale → re-select the first filled
slot (slot-00) after compacting.
Thumbnails: clear every slot's local PNG first (slot ids are positional/reused). Factory-derived
kept presets resolve their curated bundled art immediately via the stable thumbnail key; user-saved
kept presets re-render via the existing backfill on the next library open (brief placeholder). No
orphaned PNGs (consistent with resetAllToFactory).
Sync: the new slot layout persists through the backend and propagates via iCloud-KVS to the other devices, whose selection-reconcile + ATV rotation follow automatically.
Components¶
PresetSlots.keepOnlyPinned()(pure, host-tested): the compaction + pin-remap + no-op guard.SlotStore.keepOnlyPinned(): calls the model + persists.AppState.keepOnlyPinned(): clears slot thumbnails, calls the store, re-selectsslot-00.- Library overflow menu: the two destructive bulk actions consolidate into a
•••menu in the topbar — "Keep only pinned" (disabled when nothing is pinned) and "Reset all Presets" — each behind its existing/own confirmation alert. Replaces the standalone Reset (↺) button to keep the topbar uncluttered.
Testing¶
- Host-unit (TDD):
PresetSlots.keepOnlyPinned— compacts pinned to the front in order, empties the rest, remaps pins to the new ids, preservesuserOwned, and is a no-op when nothing is pinned.AppState.keepOnlyPinned— re-selectsslot-00, no-op guard.SlotStorepersists. - Build-verified: the library overflow menu on iOS/tvOS/macOS.
Non-Goals¶
- No per-preset "restore from factory" (Reset all remains the factory path).
- No thumbnail re-keying on move (backfill re-renders; accepted, consistent with migration).
Self-Review¶
Slot model + ids reuse the existing slot-NN convention; pins/rotation semantics unchanged
(all pinned → all filled). The no-op-when-nothing-pinned guard is explicit (model + UI). Scope is a
single small action; no decomposition needed.