diff --git a/CHANGELOG.md b/CHANGELOG.md index 73970f0..85e14b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- **Library:** after removing a title the selection stays at its place (on the title that moved + up into it, or the new last one) instead of jumping to the top of the list. - **Config (F6):** the default quality profile and root folder are chosen from the lists Radarr and Sonarr have, loaded with the address and API key in the fields (when the view opens, and again shortly after either is changed, before saving). A configured value the server does not diff --git a/internal/ui/library.go b/internal/ui/library.go index a601ce0..ff6fc22 100644 --- a/internal/ui/library.go +++ b/internal/ui/library.go @@ -391,9 +391,9 @@ func (v *libraryView) render() { } func (v *libraryView) renderPane(p *libPane) { - selID := -1 + selID, prevRow := -1, 0 if r, _ := p.table.GetSelection(); r >= 1 && r <= len(p.shown) { - selID = p.shown[r-1].id + selID, prevRow = p.shown[r-1].id, r } p.shown = filterItems(p.all, p.filter, p.text, p.attr) @@ -408,6 +408,13 @@ func (v *libraryView) renderPane(p *libPane) { selRow = i + 1 } } + if selRow == 0 && selID != -1 && !containsItem(p.all, selID) { + // The selected title is gone from the library (removed): stay at its + // place, on the title that moved up into it (or the new last one), + // instead of jumping to the top. A title that is only filtered out + // still starts the list from the top. + selRow = min(prevRow, len(p.shown)) + } placeholder := "Nothing matches the filter" switch { case p.err != nil && !p.loaded: @@ -437,6 +444,15 @@ func (v *libraryView) renderPane(p *libPane) { p.table.SetTitle(title + " ") } +func containsItem(items []libItem, id int) bool { + for _, it := range items { + if it.id == id { + return true + } + } + return false +} + func (v *libraryView) renderDetails() { if it, ok := v.selected(); ok { v.details.Show(it.details, it.cover) diff --git a/internal/ui/library_test.go b/internal/ui/library_test.go index 5936a5b..72cbd0a 100644 --- a/internal/ui/library_test.go +++ b/internal/ui/library_test.go @@ -209,3 +209,51 @@ func TestListedValues(t *testing.T) { t.Errorf("empty pool: %v", got) } } + +func TestRemovingATitleKeepsThePlace(t *testing.T) { + a := newTestApp(t) + v := newLibraryView(a, a.radarr, a.sonarr) + p := v.panes[0] + items := func(ids ...int) []libItem { + var out []libItem + for _, id := range ids { + out = append(out, libItem{id: id, title: string(rune('A' + id)), label: string(rune('A' + id))}) + } + return out + } + selectedID := func() int { + r, _ := p.table.GetSelection() + return p.shown[r-1].id + } + p.all, p.loaded = items(1, 2, 3, 4, 5), true + v.renderPane(p) + + // Remove the third title: the fourth moves up into its place and is selected. + p.table.Select(3, 0) + p.all = items(1, 2, 4, 5) + v.renderPane(p) + if r, _ := p.table.GetSelection(); r != 3 || selectedID() != 4 { + t.Errorf("after removing the third: row %d, title %d", r, selectedID()) + } + // Remove the last title: the new last one is selected. + p.table.Select(4, 0) + p.all = items(1, 2, 4) + v.renderPane(p) + if r, _ := p.table.GetSelection(); r != 3 || selectedID() != 4 { + t.Errorf("after removing the last: row %d, title %d", r, selectedID()) + } + // A reload that keeps the title keeps it selected, wherever it is now. + p.table.Select(2, 0) // title 2 + p.all = items(0, 1, 2, 4) + v.renderPane(p) + if selectedID() != 2 { + t.Errorf("kept title not followed: %d", selectedID()) + } + // A title that is only filtered out still starts the list from the top. + p.table.Select(4, 0) // title 4 + p.text = "B" // title 1 only: 4 is filtered out, not removed + v.renderPane(p) + if r, _ := p.table.GetSelection(); r != 1 { + t.Errorf("filtered out: row %d, want the top", r) + } +}