Library: status and property filters follow the focused list
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,7 +67,7 @@ Notes:
|
|||||||
|
|
||||||
### 4.2 v0.2 (done, M8)
|
### 4.2 v0.2 (done, M8)
|
||||||
|
|
||||||
- **Library view:** movies (top) and series (below) with the details of the selected item on the right; Tab switches between the two lists. Filters (all, missing, downloaded, monitored, unmonitored), a file-property filter (`F`: quality 2160p/1080p/720p/SD, codec x265/x264/AV1, HDR/DV/SDR, audio format Atmos/TrueHD/DTS:X/DTS-HD/DTS/DD+/DD/AAC/FLAC/Opus, audio language; changes apply live to the list behind the dialog, including while moving through an open dropdown, and Esc restores the previous filter; each dropdown only lists options that, together with the other chosen criteria, still match at least one item (series files are read when the dialog opens so this is accurate)) apply to both lists; the text filter (`/`) only acts on the focused list, and each list keeps its own text. Movie properties come from the movie list; series properties are read per series from `GET /episodefile?seriesId=` only while a property filter is active, cached while a series' file count and size stay the same, and a series matches when any of its files does. `t` toggles monitoring, `s` starts an automatic search, `i`/Enter opens the release search (movies) or the drill-down (series).
|
- **Library view:** movies (top) and series (below) with the details of the selected item on the right; Tab switches between the two lists. Filters (status: all, missing, downloaded, monitored, unmonitored), a file-property filter (`F`: quality 2160p/1080p/720p/SD, codec x265/x264/AV1, HDR/DV/SDR, audio format Atmos/TrueHD/DTS:X/DTS-HD/DTS/DD+/DD/AAC/FLAC/Opus, audio language; changes apply live to the list behind the dialog, including while moving through an open dropdown, and Esc restores the previous filter; each dropdown only lists options that, together with the other chosen criteria, still match at least one item (series files are read when the dialog opens so this is accurate)) act only on the focused list, and each list keeps its own status filter, property filter and text; the lists show their active filters in their titles. Movie properties come from the movie list; series properties are read per series from `GET /episodefile?seriesId=` only while a property filter is active, cached while a series' file count and size stay the same, and a series matches when any of its files does. `t` toggles monitoring, `s` starts an automatic search, `i`/Enter opens the release search (movies) or the drill-down (series).
|
||||||
- **Series drill-down:** seasons and episodes, toggle monitoring per season/episode, trigger season/episode search and release search.
|
- **Series drill-down:** seasons and episodes, toggle monitoring per season/episode, trigger season/episode search and release search.
|
||||||
|
|
||||||
### 4.3 Later / ideas
|
### 4.3 Later / ideas
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ const helpText = `[yellow]Global[-]
|
|||||||
[yellow]Library[-]
|
[yellow]Library[-]
|
||||||
Tab switch between the movies and series lists
|
Tab switch between the movies and series lists
|
||||||
f cycle filter (all, missing, downloaded, monitored, unmonitored);
|
f cycle filter (all, missing, downloaded, monitored, unmonitored);
|
||||||
the status and property filters apply to both lists
|
every list has its own filters
|
||||||
F filter by file properties: quality, codec (x265, x264, AV1),
|
F filter by file properties: quality, codec (x265, x264, AV1),
|
||||||
HDR / DV / SDR, audio (Atmos, TrueHD, DTS, DD+ ...), language.
|
HDR / DV / SDR, audio (Atmos, TrueHD, DTS, DD+ ...), language.
|
||||||
Changes apply live behind the dialog, also while you move through an
|
Changes apply live behind the dialog, also while you move through an
|
||||||
|
|||||||
+48
-44
@@ -184,6 +184,9 @@ type libPane struct {
|
|||||||
err error
|
err error
|
||||||
gen int // invalidates stale loads
|
gen int // invalidates stale loads
|
||||||
text string // text filter, set with / while this pane is the current one
|
text string // text filter, set with / while this pane is the current one
|
||||||
|
// Each list has its own filters.
|
||||||
|
filter int // status preset, index into libFilters
|
||||||
|
attr attrFilter // file properties
|
||||||
}
|
}
|
||||||
|
|
||||||
// libraryView shows the movies of Radarr above the series of Sonarr, with the
|
// libraryView shows the movies of Radarr above the series of Sonarr, with the
|
||||||
@@ -200,9 +203,7 @@ type libraryView struct {
|
|||||||
panes [2]*libPane
|
panes [2]*libPane
|
||||||
|
|
||||||
// UI-goroutine state.
|
// UI-goroutine state.
|
||||||
cur libKind // the pane that has (or last had) focus
|
cur libKind // the pane that has (or last had) focus
|
||||||
filter int
|
|
||||||
attr attrFilter
|
|
||||||
|
|
||||||
// File properties of series, fetched on demand and cached. An entry is
|
// File properties of series, fetched on demand and cached. An entry is
|
||||||
// reused while the series' file count and size stay the same.
|
// reused while the series' file count and size stay the same.
|
||||||
@@ -276,7 +277,8 @@ func (v *libraryView) handleKey(ev *tcell.EventKey) *tcell.EventKey {
|
|||||||
case '/':
|
case '/':
|
||||||
v.app.app.SetFocus(v.input)
|
v.app.app.SetFocus(v.input)
|
||||||
case 'f':
|
case 'f':
|
||||||
v.filter = (v.filter + 1) % len(libFilters)
|
p := v.pane()
|
||||||
|
p.filter = (p.filter + 1) % len(libFilters)
|
||||||
v.render()
|
v.render()
|
||||||
case 'F':
|
case 'F':
|
||||||
v.openFilterDialog()
|
v.openFilterDialog()
|
||||||
@@ -368,17 +370,7 @@ func (v *libraryView) render() {
|
|||||||
for _, p := range v.panes {
|
for _, p := range v.panes {
|
||||||
v.renderPane(p)
|
v.renderPane(p)
|
||||||
}
|
}
|
||||||
filters := "[yellow]" + libFilters[v.filter].name + "[-]"
|
v.header.SetText("[::b]Library[::-] · Tab switches list · each list has its own filters")
|
||||||
if v.attr.active() {
|
|
||||||
filters += " + [yellow]" + v.attr.String() + "[-]"
|
|
||||||
if v.filePending > 0 {
|
|
||||||
filters += fmt.Sprintf(" [gray](reading %d series…)[-]", v.filePending)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v.header.SetText(fmt.Sprintf("[::b]Library[::-] · %s %d/%d · %s %d/%d · filter: %s",
|
|
||||||
kindMovies, len(v.panes[kindMovies].shown), len(v.panes[kindMovies].all),
|
|
||||||
kindSeries, len(v.panes[kindSeries].shown), len(v.panes[kindSeries].all),
|
|
||||||
filters))
|
|
||||||
v.renderDetails()
|
v.renderDetails()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +379,7 @@ func (v *libraryView) renderPane(p *libPane) {
|
|||||||
if r, _ := p.table.GetSelection(); r >= 1 && r <= len(p.shown) {
|
if r, _ := p.table.GetSelection(); r >= 1 && r <= len(p.shown) {
|
||||||
selID = p.shown[r-1].id
|
selID = p.shown[r-1].id
|
||||||
}
|
}
|
||||||
p.shown = filterItems(p.all, v.filter, p.text, v.attr)
|
p.shown = filterItems(p.all, p.filter, p.text, p.attr)
|
||||||
|
|
||||||
p.table.Clear()
|
p.table.Clear()
|
||||||
header(p.table, 0, "Title", "Status", "Size")
|
header(p.table, 0, "Title", "Status", "Size")
|
||||||
@@ -413,11 +405,20 @@ func (v *libraryView) renderPane(p *libPane) {
|
|||||||
if selRow == 0 {
|
if selRow == 0 {
|
||||||
p.table.ScrollToBeginning()
|
p.table.ScrollToBeginning()
|
||||||
}
|
}
|
||||||
title := fmt.Sprintf(" %s ", p.kind)
|
title := fmt.Sprintf(" %s %d/%d", p.kind, len(p.shown), len(p.all))
|
||||||
if p.err != nil && p.loaded {
|
if p.filter > 0 {
|
||||||
title = fmt.Sprintf(" %s [red](outdated)[-] ", p.kind)
|
title += " · [yellow]" + libFilters[p.filter].name + "[-]"
|
||||||
}
|
}
|
||||||
p.table.SetTitle(title)
|
if p.attr.active() {
|
||||||
|
title += " · [yellow]" + p.attr.String() + "[-]"
|
||||||
|
if p.kind == kindSeries && v.filePending > 0 {
|
||||||
|
title += fmt.Sprintf(" [gray](reading %d…)[-]", v.filePending)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.err != nil && p.loaded {
|
||||||
|
title += " [red](outdated)[-]"
|
||||||
|
}
|
||||||
|
p.table.SetTitle(title + " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *libraryView) renderDetails() {
|
func (v *libraryView) renderDetails() {
|
||||||
@@ -499,32 +500,35 @@ func (v *libraryView) run(doing, done string, fn func(ctx context.Context) error
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *libraryView) setAttrFilter(f attrFilter) {
|
// openFilterDialog opens the property filter for the focused list. For series
|
||||||
v.attr = f
|
// the files of all series are read so the dialog can leave out options that
|
||||||
v.render()
|
// would match nothing.
|
||||||
v.loadSeriesFiles(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// openFilterDialog opens the property filter. The files of all series are read
|
|
||||||
// so the dialog can leave out options that would match nothing.
|
|
||||||
func (v *libraryView) openFilterDialog() {
|
func (v *libraryView) openFilterDialog() {
|
||||||
v.refreshDialog = v.app.openAttrFilter(v.attr, v.filterPool, v.setAttrFilter, func() { v.refreshDialog = nil })
|
p := v.pane()
|
||||||
v.loadSeriesFiles(true)
|
set := func(f attrFilter) {
|
||||||
}
|
p.attr = f
|
||||||
|
v.render()
|
||||||
// filterPool lists the files of the items that pass the status and text
|
if p.kind == kindSeries {
|
||||||
// filters, for the filter dialog.
|
v.loadSeriesFiles(false)
|
||||||
func (v *libraryView) filterPool() filterPool {
|
|
||||||
var p filterPool
|
|
||||||
for _, pane := range v.panes {
|
|
||||||
for _, it := range filterItems(pane.all, v.filter, pane.text, attrFilter{}) {
|
|
||||||
if !it.attrsKnown {
|
|
||||||
p.unknown = true
|
|
||||||
}
|
|
||||||
p.files = append(p.files, it.attrs)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return p
|
v.refreshDialog = v.app.openAttrFilter(p.attr, func() filterPool { return v.filterPool(p) }, set, func() { v.refreshDialog = nil })
|
||||||
|
if p.kind == kindSeries {
|
||||||
|
v.loadSeriesFiles(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterPool lists the files of the items of one list that pass its status
|
||||||
|
// and text filters, for the filter dialog.
|
||||||
|
func (v *libraryView) filterPool(p *libPane) filterPool {
|
||||||
|
var pool filterPool
|
||||||
|
for _, it := range filterItems(p.all, p.filter, p.text, attrFilter{}) {
|
||||||
|
if !it.attrsKnown {
|
||||||
|
pool.unknown = true
|
||||||
|
}
|
||||||
|
pool.files = append(pool.files, it.attrs)
|
||||||
|
}
|
||||||
|
return pool
|
||||||
}
|
}
|
||||||
|
|
||||||
// withCachedFiles fills in the file properties of a series from the cache.
|
// withCachedFiles fills in the file properties of a series from the cache.
|
||||||
@@ -543,7 +547,7 @@ func (v *libraryView) withCachedFiles(it libItem) libItem {
|
|||||||
// but only while a property filter is active or the filter dialog is open
|
// but only while a property filter is active or the filter dialog is open
|
||||||
// (it costs one request per series).
|
// (it costs one request per series).
|
||||||
func (v *libraryView) loadSeriesFiles(force bool) {
|
func (v *libraryView) loadSeriesFiles(force bool) {
|
||||||
if !force && !v.attr.active() {
|
if !force && !v.panes[kindSeries].attr.active() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p := v.panes[kindSeries]
|
p := v.panes[kindSeries]
|
||||||
|
|||||||
Reference in New Issue
Block a user