diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ed30f..fa64570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ - **Trending, most watched and anticipated** movies and series from Trakt: with an empty search box, Movies and Series show trending now, most watched this week, anticipated, popular and (movies) - the US box office; `[` `]` switch lists, `r` reloads. Titles in the library are ticked, the others + the US box office, each title with its number ("1.2k watching", "on 18k lists", "$48M last + weekend") right-aligned in grey; `[` `]` switch lists, `r` reloads. Titles in the library are ticked, the others can be added and searched like search results. Needs a free Trakt client ID, set in the new Trakt section of the Config view (`trakt.client_id`, `ARR_TUI_TRAKT_CLIENT_ID`). diff --git a/docs/screenshots/discover.png b/docs/screenshots/discover.png index 3bf3a2b..ea80942 100644 Binary files a/docs/screenshots/discover.png and b/docs/screenshots/discover.png differ diff --git a/internal/ui/discover_test.go b/internal/ui/discover_test.go index 18bd9b3..1e53c7a 100644 --- a/internal/ui/discover_test.go +++ b/internal/ui/discover_test.go @@ -8,6 +8,8 @@ import ( "time" "github.com/gdamore/tcell/v2" + + "git.stinnesbeck.com/nils/arr-tui/internal/radarr" ) func TestTraktListsInTheSearchView(t *testing.T) { @@ -86,3 +88,43 @@ func TestTraktListsInTheSearchView(t *testing.T) { t.Errorf("search row %q", main) } } + +func TestListNumbersAreRightAligned(t *testing.T) { + sim := tcell.NewSimulationScreen("") + if err := sim.Init(); err != nil { + t.Fatal(err) + } + defer sim.Fini() + sim.SetSize(60, 6) + + a := newTestApp(t) + v := a.searches[0] + a.trakt.Reconfigure("id") + v.discover[0] = &discovered{at: time.Now(), + results: []result{movieResult{Movie: radarr.Movie{Title: "Dune", Year: 2021}}, movieResult{Movie: radarr.Movie{Title: "A Much Longer Title", Year: 2024}}}, + notes: []string{"1.2k watching", "5 watching"}} + v.showList() + + ends := func(width int) []int { + sim.Clear() + l := notesList{v.list, v} + l.SetRect(0, 0, width, 6) + l.Draw(sim) + var out []int + for y := 1; y <= 2; y++ { + row := strings.TrimRight(rowText(sim, y, 0, width), " │") + out = append(out, len([]rune(row))) + if !strings.HasSuffix(row, "watching") { + t.Errorf("row %d does not end with its number: %q", y, row) + } + } + return out + } + if e := ends(60); e[0] != e[1] { + t.Errorf("numbers end in columns %v, want the same", e) + } + // After a resize they line up at the new edge. + if e := ends(44); e[0] != e[1] || e[0] > 44 { + t.Errorf("after resize: numbers end in columns %v", e) + } +} diff --git a/internal/ui/search.go b/internal/ui/search.go index 203562e..a16e6f4 100644 --- a/internal/ui/search.go +++ b/internal/ui/search.go @@ -36,6 +36,7 @@ type searchView struct { results []result notes []string // per result, a Trakt list's number ("1.2k watching"); nil for searches placeholder string // shown when there are no results + notesWidth int // list width the rows were laid out for timer *time.Timer cancel context.CancelFunc gen int @@ -71,7 +72,7 @@ func newSearchView(app *App, be backend) *searchView { left := tview.NewFlex().SetDirection(tview.FlexRow). AddItem(v.input, 1, 0, true). - AddItem(v.list, 0, 1, false) + AddItem(notesList{v.list, v}, 0, 1, false) v.root = tview.NewFlex(). AddItem(left, 0, 1, true). AddItem(v.details, 0, 1, false) @@ -279,12 +280,10 @@ func (v *searchView) activate() { // render rebuilds the list, selecting index sel. func (v *searchView) render(sel int) { v.list.Clear() - for i, r := range v.results { - label := r.label() - if i < len(v.notes) && v.notes[i] != "" { - label += " [gray]" + tview.Escape(v.notes[i]) + "[-]" - } - v.list.AddItem(label, "", 0, nil) + _, _, width, _ := v.list.GetInnerRect() + v.notesWidth = width + for i := range v.results { + v.list.AddItem(v.itemText(i, width), "", 0, nil) } if len(v.results) == 0 { if v.placeholder != "" { @@ -298,6 +297,41 @@ func (v *searchView) render(sel int) { v.showDetails(sel) } +// itemText is the row of result i in a list width columns wide: the label, +// and a Trakt list's number (if any) in grey against the right edge, so the +// numbers of all rows line up. A label too long for that gets the number +// right after it. +func (v *searchView) itemText(i, width int) string { + label := v.results[i].label() + if i >= len(v.notes) || v.notes[i] == "" { + return label + } + note := tview.Escape(v.notes[i]) + gap := width - tview.TaggedStringWidth(label) - tview.TaggedStringWidth(note) + if gap < 2 { + gap = 2 + } + return label + strings.Repeat(" ", gap) + "[gray]" + note + "[-]" +} + +// notesList draws the results list, first laying the rows out again when the +// width changed (the numbers of a Trakt list sit against the right edge). +type notesList struct { + *tview.List + v *searchView +} + +func (n notesList) Draw(screen tcell.Screen) { + v := n.v + if _, _, width, _ := v.list.GetInnerRect(); width != v.notesWidth && len(v.notes) > 0 { + v.notesWidth = width + for i := range v.results { + v.list.SetItemText(i, v.itemText(i, width), "") + } + } + n.List.Draw(screen) +} + func (v *searchView) showDetails(i int) { if i < 0 || i >= len(v.results) { v.details.Clear()