feat(saves): redesign saves tab with slot-based collapsible layout#220
Merged
danielcopper merged 14 commits intomainfrom Apr 8, 2026
Merged
feat(saves): redesign saves tab with slot-based collapsible layout#220danielcopper merged 14 commits intomainfrom
danielcopper merged 14 commits intomainfrom
Conversation
) Replace the two-column saves tab (files left, slots right) with a slot-centric collapsible panel layout. Active slot is expanded at the top, inactive slots are collapsed and lazy-load server saves on expand. Backend: - Add get_slot_saves callable for per-slot server save listing - Add switch_slot callable with guards (pending uploads, server reachable) and immediate download sync on slot change - 12 new tests covering happy paths, error cases, and edge cases Frontend: - Extract SavesTab.tsx from RomMGameInfoPanel.tsx (~280 lines removed) - Collapsible slot panels with active/server/local badges - Replace colored dots with colored text status labels - Lazy-load inactive slot details via getSlotSaves - Inline error feedback on failed slot switches Also fix gear button focus visibility — use border-color instead of outline so controller focus highlight matches native Steam buttons.
…focus The useEffect slots loader used || which coerced null (legacy mode) to "default", immediately overwriting the user's legacy slot choice. Changed to ?? to match refreshSlotState which already handled this correctly. Also add explicit .gpfocus rule for gear buttons — Steam does not provide a default focus style for DialogButton.
The slots useEffect used ?? which treats null as "use fallback", but null is a valid value meaning legacy mode. Changed to explicit === undefined check, matching the pattern in refreshSlotState. Also add !important to gear button .gpfocus border-color and background to override Steam DialogButton default styles.
…istence - handleNewSlot now calls switchSlot (not setGameSlot) for both legacy and named slots, ensuring pre-checks + immediate sync on every switch - Legacy slot persisted as "" key in slots dict so it survives getSaveSlots refresh cycles - get_save_slots maps server legacy saves to "" instead of "default" - switch_slot filter normalizes ""/None for legacy save matching - onSlotSwitched uses explicit === "" check instead of || for legacy - Redundant "" panel filtered when already in legacy mode - Error feedback shown near + New Slot button on switch failure - Remove dead onNewSlot callback and debug logging
When switching slots, local state must match the new slot's server state: - New slot has server saves: download them, replace local files - New slot is empty: delete local save files (fresh start) - Never upload — saves are not carried between slots Also fix _check_slot_switch_readiness to not block on never-synced files (they get deleted during the switch anyway). Update last_sync_check_at after every switch.
match_local_to_server_saves passed all server saves unfiltered when active_slot was None (legacy mode), causing saves from named slots to appear as "Server newer" in the legacy view. Now only matches saves without a slot assignment in legacy mode.
Legacy mode (active_slot=None) now only matches saves without a slot assignment. Update tests that expected all saves to match regardless of slot, and add a test verifying unslotted saves still match.
- Extract computeSyncSummary, renderActiveSlotBody, renderInactiveSlotBody, renderDeviceSyncInfo helpers to reduce SlotPanel cognitive complexity - Extract _SYNC_DISABLED_MSG constant for duplicate literal - Replace nested ternaries with if/else blocks - Use Number.isNaN instead of isNaN - Remove unnecessary non-null assertions - Refactor negated filter condition to positive form
- Replace negated condition with positive form in pluralization - Replace nested ternary in bodyChildren with if/else block
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Closes #206
SavesTab.tsxfromRomMGameInfoPanel.tsx(280 lines removed from parent)getSlotSavesswitchSlotwith pre-checks (pending uploads, server reachable) and immediate sync.gpfocusstyling for controller focus visibilityNew backend callables
get_slot_saves(rom_id, slot)— fetch server save files for a specific slot (lazy-load on panel expand)switch_slot(rom_id, new_slot)— guarded slot switch with immediate download syncFrontend
SavesTab.tsxcomponent withSlotPanelsub-componentsSlotSaveFile,SlotSavesResponse,SwitchSlotResponsestyleInjector.tsTest plan
last_sync_check_atupdates on every switch