Revert the scrolling calendar: back to moving the two-week window with [ and ]
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+59
-187
@@ -18,12 +18,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
calendarPast = 7 // days before today loaded at the start
|
||||
calendarAhead = 28 // days after today loaded at the start
|
||||
calendarChunk = 28 // days added when scrolling reaches either end
|
||||
calendarMaxBack = 180 // how far back, and
|
||||
calendarMaxFwd = 400 // how far ahead the list can grow
|
||||
calendarEdge = 6 // rows from an end at which more is loaded
|
||||
calendarDays = 14 // length of the window
|
||||
calendarStep = 7 // `[` and `]` move by this many days
|
||||
)
|
||||
|
||||
// calEntry is one line of the calendar: a movie release or an episode airing.
|
||||
@@ -170,9 +166,7 @@ func dayLabel(day, today time.Time) (date, relative string) {
|
||||
return date, ""
|
||||
}
|
||||
|
||||
// calendarView lists the movies and episodes coming up as one scrolling list.
|
||||
// It starts with a few days back and a month ahead and loads more days
|
||||
// whenever the selection gets close to either end.
|
||||
// calendarView lists the movies and episodes coming up in a two-week window.
|
||||
type calendarView struct {
|
||||
app *App
|
||||
radarr *radarr.Client
|
||||
@@ -184,14 +178,12 @@ type calendarView struct {
|
||||
details *detailPane
|
||||
|
||||
// UI-goroutine state.
|
||||
offset int // days from today to the first day shown; moved with [ and ]
|
||||
unmonitored bool
|
||||
from, to time.Time // the days loaded so far
|
||||
entries []calEntry
|
||||
rows []calRow
|
||||
loaded bool
|
||||
loading [2]bool // loading more at the [top, bottom]
|
||||
gen int // invalidates loads from before a reset
|
||||
reposition bool // put today at the top after the next render
|
||||
gen int
|
||||
}
|
||||
|
||||
// calRow is a table row: a day header (entry == nil) or an entry.
|
||||
@@ -207,17 +199,14 @@ func newCalendarView(app *App, r *radarr.Client, s *sonarr.Client) *calendarView
|
||||
table: tview.NewTable().SetSelectable(true, false).SetFixed(0, 0),
|
||||
details: newDetailPane(app),
|
||||
}
|
||||
v.table.SetBorder(true)
|
||||
v.table.SetBorder(true).SetTitle(" Upcoming ")
|
||||
v.root = tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(v.header, 1, 0, false).
|
||||
AddItem(tview.NewFlex().
|
||||
AddItem(v.table, 0, 3, true).
|
||||
AddItem(v.details, 0, 2, false), 0, 1, true)
|
||||
|
||||
v.table.SetSelectionChangedFunc(func(int, int) {
|
||||
v.renderDetails()
|
||||
v.loadNearEnd()
|
||||
})
|
||||
v.table.SetSelectionChangedFunc(func(int, int) { v.renderDetails() })
|
||||
v.table.SetSelectedFunc(func(int, int) { v.open() })
|
||||
v.table.SetInputCapture(v.handleKey)
|
||||
v.render()
|
||||
@@ -225,130 +214,50 @@ func newCalendarView(app *App, r *radarr.Client, s *sonarr.Client) *calendarView
|
||||
}
|
||||
|
||||
func (v *calendarView) handleKey(ev *tcell.EventKey) *tcell.EventKey {
|
||||
switch ev.Key() {
|
||||
case tcell.KeyDown, tcell.KeyPgDn, tcell.KeyEnd:
|
||||
v.loadMore(true) // pressing down at the last entry cannot change the selection, so ask here
|
||||
case tcell.KeyUp, tcell.KeyPgUp, tcell.KeyHome:
|
||||
v.loadMore(false)
|
||||
case tcell.KeyRune:
|
||||
switch ev.Rune() {
|
||||
case 't':
|
||||
v.today()
|
||||
case 'u':
|
||||
v.unmonitored = !v.unmonitored
|
||||
v.reload()
|
||||
case 'r':
|
||||
v.reload()
|
||||
case 'i':
|
||||
v.open()
|
||||
case 's':
|
||||
v.search()
|
||||
default:
|
||||
return ev
|
||||
}
|
||||
return nil
|
||||
if ev.Key() != tcell.KeyRune {
|
||||
return ev
|
||||
}
|
||||
return ev
|
||||
}
|
||||
|
||||
func (v *calendarView) activate() {
|
||||
if !v.loaded {
|
||||
switch ev.Rune() {
|
||||
case '[':
|
||||
v.offset -= calendarStep
|
||||
v.reload()
|
||||
} else {
|
||||
v.refresh()
|
||||
case ']':
|
||||
v.offset += calendarStep
|
||||
v.reload()
|
||||
case 't':
|
||||
v.offset = 0
|
||||
v.reload()
|
||||
case 'u':
|
||||
v.unmonitored = !v.unmonitored
|
||||
v.reload()
|
||||
case 'r':
|
||||
v.reload()
|
||||
case 'i':
|
||||
v.open()
|
||||
case 's':
|
||||
v.search()
|
||||
default:
|
||||
return ev
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// reload forgets everything and loads the initial range, with today at the top.
|
||||
// window returns the first and last day shown.
|
||||
func (v *calendarView) window(now time.Time) (from, to time.Time) {
|
||||
from = localDay(now).AddDate(0, 0, v.offset)
|
||||
return from, from.AddDate(0, 0, calendarDays-1)
|
||||
}
|
||||
|
||||
func (v *calendarView) activate() { v.reload() }
|
||||
|
||||
// reload fetches both calendars in parallel.
|
||||
func (v *calendarView) reload() {
|
||||
v.gen++
|
||||
v.loading = [2]bool{}
|
||||
today := localDay(time.Now())
|
||||
v.from, v.to = today.AddDate(0, 0, -calendarPast), today.AddDate(0, 0, calendarAhead-1)
|
||||
v.entries, v.loaded, v.reposition = nil, false, true
|
||||
v.render()
|
||||
v.fetch(v.from, v.to, v.gen, func(entries []calEntry) {
|
||||
v.entries, v.loaded = entries, true
|
||||
})
|
||||
}
|
||||
|
||||
// refresh reloads the range loaded so far, keeping the position.
|
||||
func (v *calendarView) refresh() {
|
||||
gen := v.gen
|
||||
v.fetch(v.from, v.to, gen, func(entries []calEntry) { v.entries = entries })
|
||||
}
|
||||
|
||||
// loadNearEnd loads more days when the selection is close to either end.
|
||||
func (v *calendarView) loadNearEnd() {
|
||||
if !v.loaded {
|
||||
return
|
||||
}
|
||||
r, _ := v.table.GetSelection()
|
||||
if r >= len(v.rows)-calendarEdge {
|
||||
v.loadMore(true)
|
||||
}
|
||||
if r <= calendarEdge {
|
||||
v.loadMore(false)
|
||||
}
|
||||
}
|
||||
|
||||
// loadMore extends the loaded range by a chunk of days downwards (later) or
|
||||
// upwards (earlier), up to the limits.
|
||||
func (v *calendarView) loadMore(later bool) {
|
||||
idx := 0
|
||||
if later {
|
||||
idx = 1
|
||||
}
|
||||
if !v.loaded || v.loading[idx] {
|
||||
return
|
||||
}
|
||||
today := localDay(time.Now())
|
||||
var from, to time.Time
|
||||
if later {
|
||||
if v.to.Sub(today) >= calendarMaxFwd*24*time.Hour {
|
||||
return
|
||||
}
|
||||
from, to = v.to.AddDate(0, 0, 1), v.to.AddDate(0, 0, calendarChunk)
|
||||
} else {
|
||||
if today.Sub(v.from) >= calendarMaxBack*24*time.Hour {
|
||||
return
|
||||
}
|
||||
from, to = v.from.AddDate(0, 0, -calendarChunk), v.from.AddDate(0, 0, -1)
|
||||
}
|
||||
v.loading[idx] = true
|
||||
v.render() // shows "loading" in the title
|
||||
gen := v.gen
|
||||
v.fetch(from, to, gen, func(entries []calEntry) {
|
||||
v.entries = mergeEntries(v.entries, entries)
|
||||
if later {
|
||||
v.to = to
|
||||
} else {
|
||||
v.from = from
|
||||
}
|
||||
}, func() { v.loading[idx] = false })
|
||||
}
|
||||
|
||||
// mergeEntries adds new entries to the sorted list, skipping ones already there.
|
||||
func mergeEntries(have, add []calEntry) []calEntry {
|
||||
seen := make(map[string]bool, len(have))
|
||||
for _, e := range have {
|
||||
seen[e.key()] = true
|
||||
}
|
||||
for _, e := range add {
|
||||
if !seen[e.key()] {
|
||||
have = append(have, e)
|
||||
}
|
||||
}
|
||||
sortEntries(have)
|
||||
return have
|
||||
}
|
||||
|
||||
// fetch loads the entries between from and to in the background. apply runs
|
||||
// on the UI goroutine with the entries when at least one service answered;
|
||||
// done (optional) always runs afterwards. Results from before a reset are dropped.
|
||||
func (v *calendarView) fetch(from, to time.Time, gen int, apply func([]calEntry), done ...func()) {
|
||||
unmonitored := v.unmonitored
|
||||
gen, unmonitored := v.gen, v.unmonitored
|
||||
now := time.Now()
|
||||
from, to := v.window(now)
|
||||
v.render()
|
||||
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
@@ -384,35 +293,19 @@ func (v *calendarView) fetch(from, to time.Time, gen int, apply func([]calEntry)
|
||||
if gen != v.gen {
|
||||
return
|
||||
}
|
||||
for _, d := range done {
|
||||
d() // before rendering, so the title no longer says "loading"
|
||||
}
|
||||
for _, err := range []error{movieErr, tvErr} {
|
||||
if err != nil {
|
||||
v.app.SetMessage("[red]Calendar: " + tview.Escape(err.Error()))
|
||||
}
|
||||
}
|
||||
if movieErr == nil || tvErr == nil { // show what we got
|
||||
apply(entries)
|
||||
v.entries, v.loaded = entries, true
|
||||
}
|
||||
v.render()
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// today selects the first entry of today (or the next day with one), with
|
||||
// today's header at the top. If today is out of the loaded range the list is
|
||||
// reset around it.
|
||||
func (v *calendarView) today() {
|
||||
today := localDay(time.Now())
|
||||
if today.Before(v.from) || today.After(v.to) {
|
||||
v.reload()
|
||||
return
|
||||
}
|
||||
v.reposition = true
|
||||
v.render()
|
||||
}
|
||||
|
||||
func (v *calendarView) selected() (*calEntry, bool) {
|
||||
r, _ := v.table.GetSelection()
|
||||
if r < 0 || r >= len(v.rows) || v.rows[r].entry == nil {
|
||||
@@ -423,13 +316,12 @@ func (v *calendarView) selected() (*calEntry, bool) {
|
||||
|
||||
func (v *calendarView) render() {
|
||||
selKey := ""
|
||||
oldSel, _ := v.table.GetSelection()
|
||||
oldOffset, _ := v.table.GetOffset()
|
||||
if e, ok := v.selected(); ok {
|
||||
selKey = e.key()
|
||||
}
|
||||
now := time.Now()
|
||||
today := localDay(now)
|
||||
from, to := v.window(now)
|
||||
|
||||
v.rows = v.rows[:0]
|
||||
var lastDay time.Time
|
||||
@@ -443,11 +335,8 @@ func (v *calendarView) render() {
|
||||
}
|
||||
|
||||
v.table.Clear()
|
||||
selRow, firstEntry, todayRow := -1, -1, -1
|
||||
selRow, firstEntry := 0, -1
|
||||
for i, r := range v.rows {
|
||||
if todayRow < 0 && !r.day.Before(today) {
|
||||
todayRow = i // the header of today, or of the next day with entries
|
||||
}
|
||||
if r.entry == nil {
|
||||
date, relative := dayLabel(r.day, today)
|
||||
v.table.SetCell(i, 0, tview.NewTableCell("[::b][yellow]"+date).SetSelectable(false))
|
||||
@@ -477,16 +366,13 @@ func (v *calendarView) render() {
|
||||
}
|
||||
}
|
||||
|
||||
title := " Upcoming · " + v.from.Format("2 Jan") + " – " + v.to.Format("2 Jan")
|
||||
title := " Upcoming · " + from.Format("2 Jan") + " – " + to.Format("2 Jan") + " "
|
||||
if v.unmonitored {
|
||||
title += " · incl. unmonitored"
|
||||
title += "· incl. unmonitored "
|
||||
}
|
||||
if v.loading[0] || v.loading[1] || !v.loaded {
|
||||
title += " · loading…"
|
||||
}
|
||||
v.table.SetTitle(title + " ")
|
||||
|
||||
if firstEntry < 0 {
|
||||
v.table.SetTitle(title)
|
||||
switch {
|
||||
case len(v.entries) == 0:
|
||||
msg := "Loading…"
|
||||
if v.loaded {
|
||||
msg = "Nothing in this period"
|
||||
@@ -494,31 +380,17 @@ func (v *calendarView) render() {
|
||||
v.table.SetCell(0, 0, tview.NewTableCell("[gray]"+msg).SetSelectable(false))
|
||||
v.table.SetSelectable(false, false)
|
||||
v.table.Select(0, 0)
|
||||
} else {
|
||||
default:
|
||||
v.table.SetSelectable(true, false)
|
||||
switch {
|
||||
case v.reposition && v.loaded:
|
||||
// Today's header at the top, the first entry from today on selected.
|
||||
v.reposition = false
|
||||
target := firstEntry
|
||||
for i := max(todayRow, 0); i < len(v.rows); i++ {
|
||||
if v.rows[i].entry != nil {
|
||||
target = i
|
||||
break
|
||||
}
|
||||
}
|
||||
v.table.Select(target, 0)
|
||||
v.table.SetOffset(max(todayRow, 0), 0)
|
||||
case selRow >= 0:
|
||||
// Keep the selection where it was on screen, also when rows were
|
||||
// added above it.
|
||||
v.table.Select(selRow, 0)
|
||||
v.table.SetOffset(max(oldOffset+selRow-oldSel, 0), 0)
|
||||
default:
|
||||
v.table.Select(firstEntry, 0)
|
||||
if selRow == 0 {
|
||||
selRow = firstEntry
|
||||
}
|
||||
v.table.Select(selRow, 0)
|
||||
if selRow == firstEntry {
|
||||
v.table.ScrollToBeginning()
|
||||
}
|
||||
}
|
||||
v.header.SetText("[::b]Calendar[::-] · scroll for more · t today · u unmonitored · Enter releases · s search")
|
||||
v.header.SetText("[::b]Calendar[::-] · [ ] move by a week · t today · u unmonitored · Enter releases · s search")
|
||||
v.renderDetails()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user