Filter by audio format; apply filter changes live behind the dialog
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -207,6 +207,17 @@ func (a *App) openModal(p tview.Primitive, width, height int) {
|
||||
a.pushOverlay(box, p, "↑↓ move · ←→ change · Enter select · Esc cancel", false, nil)
|
||||
}
|
||||
|
||||
// openSidePanel shows p at the top right so most of the view behind it stays
|
||||
// visible, for dialogs whose changes are applied live.
|
||||
func (a *App) openSidePanel(p tview.Primitive, width, height int, hints string) {
|
||||
box := tview.NewFlex().
|
||||
AddItem(nil, 0, 1, false).
|
||||
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(p, height, 0, true).
|
||||
AddItem(nil, 0, 1, false), width, 0, true)
|
||||
a.pushOverlay(box, p, hints, false, nil)
|
||||
}
|
||||
|
||||
// openFull shows p above the whole view area (the tab bar and status bar stay visible).
|
||||
func (a *App) openFull(p tview.Primitive, hints string, onClose func()) {
|
||||
a.pushOverlay(p, p, hints, true, onClose)
|
||||
|
||||
+5
-3
@@ -23,7 +23,9 @@ const helpText = `[yellow]Global[-]
|
||||
f cycle filter (all, missing, downloaded, monitored, unmonitored);
|
||||
filters apply to both lists
|
||||
F filter by file properties: quality, codec (x265, x264, AV1),
|
||||
HDR / DV / SDR, language. Series are read on demand.
|
||||
HDR / DV / SDR, audio (Atmos, TrueHD, DTS, DD+ ...), language.
|
||||
Changes apply live behind the dialog; Esc undoes them.
|
||||
Series are read on demand.
|
||||
/ text filter (Enter or Esc returns to the list)
|
||||
t toggle monitored
|
||||
s automatic search (series: all missing, asks first)
|
||||
@@ -39,8 +41,8 @@ const helpText = `[yellow]Global[-]
|
||||
Enter, g grab the selected release
|
||||
s cycle sort (best, size, age)
|
||||
f hide rejected releases
|
||||
F filter by quality, codec, HDR / DV, language (read from the
|
||||
release title, quality and custom formats)
|
||||
F filter by quality, codec, HDR / DV, audio, language (read from
|
||||
the release title, quality and custom formats)
|
||||
|
||||
[yellow]Downloads[-]
|
||||
p pause / resume the SABnzbd queue
|
||||
|
||||
+31
-13
@@ -13,7 +13,7 @@ import (
|
||||
// empty field means "any". All fields must match the same file; a series
|
||||
// matches when any of its files does.
|
||||
type attrFilter struct {
|
||||
res, codec, hdr, lang string
|
||||
res, codec, hdr, audio, lang string
|
||||
}
|
||||
|
||||
const anyOption = "any"
|
||||
@@ -22,6 +22,8 @@ var (
|
||||
resOptions = []string{"2160p", "1080p", "720p", "SD"}
|
||||
codecOptions = []string{arr.CodecX265, arr.CodecX264, arr.CodecAV1}
|
||||
hdrOptions = []string{"HDR", "DV", "SDR"}
|
||||
audioOptions = []string{arr.AudioAtmos, arr.AudioTrueHD, arr.AudioDTSX, arr.AudioDTSHD, arr.AudioDTS,
|
||||
arr.AudioDDPlus, arr.AudioDD, arr.AudioAAC, arr.AudioFLAC, arr.AudioOpus}
|
||||
)
|
||||
|
||||
func (f attrFilter) active() bool { return f != attrFilter{} }
|
||||
@@ -29,7 +31,7 @@ func (f attrFilter) active() bool { return f != attrFilter{} }
|
||||
// String lists the active criteria for the header, e.g. "1080p x265 DV".
|
||||
func (f attrFilter) String() string {
|
||||
var parts []string
|
||||
for _, p := range []string{f.res, f.codec, f.hdr, f.lang} {
|
||||
for _, p := range []string{f.res, f.codec, f.hdr, f.audio, f.lang} {
|
||||
if p != "" {
|
||||
parts = append(parts, p)
|
||||
}
|
||||
@@ -66,6 +68,9 @@ func (f attrFilter) matchFile(a arr.FileAttrs) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if f.audio != "" && !slices.Contains(a.Audio, f.audio) {
|
||||
return false
|
||||
}
|
||||
if f.lang != "" && !slices.ContainsFunc(a.Languages, func(l string) bool { return strings.EqualFold(l, f.lang) }) {
|
||||
return false
|
||||
}
|
||||
@@ -104,8 +109,10 @@ func indexOf(options []string, v string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// openAttrFilter shows the dialog to pick quality, codec, HDR and language.
|
||||
func (a *App) openAttrFilter(cur attrFilter, languages []string, apply func(attrFilter)) {
|
||||
// openAttrFilter shows the dialog to pick quality, codec, HDR, audio and
|
||||
// language. Every change is passed to set immediately so the list behind the
|
||||
// dialog follows along; Cancel and Esc restore cur.
|
||||
func (a *App) openAttrFilter(cur attrFilter, languages []string, set func(attrFilter)) {
|
||||
if cur.lang != "" && !slices.Contains(languages, cur.lang) {
|
||||
languages = append([]string{cur.lang}, languages...)
|
||||
}
|
||||
@@ -114,12 +121,14 @@ func (a *App) openAttrFilter(cur attrFilter, languages []string, apply func(attr
|
||||
lRes = "Quality"
|
||||
lCodec = "Codec"
|
||||
lHDR = "HDR"
|
||||
lAudio = "Audio"
|
||||
lLang = "Language"
|
||||
)
|
||||
form := tview.NewForm().
|
||||
AddDropDown(lRes, withAny(resOptions), indexOf(resOptions, cur.res), nil).
|
||||
AddDropDown(lCodec, withAny(codecOptions), indexOf(codecOptions, cur.codec), nil).
|
||||
AddDropDown(lHDR, withAny(hdrOptions), indexOf(hdrOptions, cur.hdr), nil).
|
||||
AddDropDown(lAudio, withAny(audioOptions), indexOf(audioOptions, cur.audio), nil).
|
||||
AddDropDown(lLang, withAny(languages), indexOf(languages, cur.lang), nil)
|
||||
|
||||
pick := func(label string, options []string) string {
|
||||
@@ -129,22 +138,31 @@ func (a *App) openAttrFilter(cur attrFilter, languages []string, apply func(attr
|
||||
}
|
||||
return options[i-1]
|
||||
}
|
||||
form.AddButton("Apply", func() {
|
||||
a.closeModal()
|
||||
apply(attrFilter{
|
||||
current := func() attrFilter {
|
||||
return attrFilter{
|
||||
res: pick(lRes, resOptions),
|
||||
codec: pick(lCodec, codecOptions),
|
||||
hdr: pick(lHDR, hdrOptions),
|
||||
audio: pick(lAudio, audioOptions),
|
||||
lang: pick(lLang, languages),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, label := range []string{lRes, lCodec, lHDR, lAudio, lLang} {
|
||||
form.GetFormItemByLabel(label).(*tview.DropDown).SetSelectedFunc(func(string, int) { set(current()) })
|
||||
}
|
||||
|
||||
cancel := func() {
|
||||
a.closeModal()
|
||||
set(cur)
|
||||
}
|
||||
form.AddButton("Apply", a.closeModal)
|
||||
form.AddButton("Reset", func() {
|
||||
a.closeModal()
|
||||
apply(attrFilter{})
|
||||
set(attrFilter{})
|
||||
})
|
||||
form.AddButton("Cancel", a.closeModal)
|
||||
form.SetCancelFunc(a.closeModal)
|
||||
form.AddButton("Cancel", cancel)
|
||||
form.SetCancelFunc(cancel)
|
||||
arrowNavigation(form)
|
||||
form.SetBorder(true).SetTitle(" Filter by properties ")
|
||||
a.openModal(form, 50, 13)
|
||||
a.openSidePanel(form, 40, 17, "↑↓ move · ←→ change (applies live) · Enter apply · Esc cancel")
|
||||
}
|
||||
|
||||
@@ -113,6 +113,8 @@ func TestAttrFilter(t *testing.T) {
|
||||
{"dv", attrFilter{hdr: "DV"}, []arr.FileAttrs{hd}, false},
|
||||
{"sdr", attrFilter{hdr: "SDR"}, []arr.FileAttrs{hd}, true},
|
||||
{"sdr excludes hdr", attrFilter{hdr: "SDR"}, []arr.FileAttrs{uhd}, false},
|
||||
{"audio", attrFilter{audio: "Atmos"}, []arr.FileAttrs{{Audio: []string{"Atmos", "TrueHD"}}}, true},
|
||||
{"audio mismatch", attrFilter{audio: "DTS"}, []arr.FileAttrs{{Audio: []string{"DD+"}}}, false},
|
||||
{"language is case-insensitive", attrFilter{lang: "german"}, []arr.FileAttrs{uhd}, true},
|
||||
{"all criteria must match one file", attrFilter{res: "1080p", codec: "x265"}, []arr.FileAttrs{hd, uhd}, false},
|
||||
{"any file of a series", attrFilter{res: "1080p", codec: "x264"}, []arr.FileAttrs{uhd, hd}, true},
|
||||
|
||||
Reference in New Issue
Block a user