package ui import ( "slices" "strings" "github.com/rivo/tview" "git.stinnesbeck.com/nils/arr-tui/internal/arr" ) // attrFilter narrows the library by properties of the downloaded files. An // 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, audio, lang string } const anyOption = "any" 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{} } // 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.audio, f.lang} { if p != "" { parts = append(parts, p) } } return strings.Join(parts, " ") } func (f attrFilter) matchFile(a arr.FileAttrs) bool { switch f.res { case "": case "SD": if a.Resolution <= 0 || a.Resolution > 576 { return false } default: if f.res != itoa(a.Resolution)+"p" { return false } } if f.codec != "" && f.codec != a.Codec { return false } switch f.hdr { case "HDR": if !a.HDR { return false } case "DV": if !a.DV { return false } case "SDR": if a.HDR || a.DV { 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 } return true } // match reports whether any of the files matches. Without files nothing matches. func (f attrFilter) match(files []arr.FileAttrs) bool { return slices.ContainsFunc(files, f.matchFile) } // languagesOf collects the languages of all known files, sorted. func languagesOf(items ...[]libItem) []string { seen := map[string]bool{} var out []string for _, list := range items { for _, it := range list { for _, a := range it.attrs { for _, l := range a.Languages { if !seen[l] { seen[l] = true out = append(out, l) } } } } } slices.Sort(out) return out } func indexOf(options []string, v string) int { if i := slices.Index(options, v); i >= 0 { return i + 1 // option 0 is "any" } return 0 } // 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...) } withAny := func(o []string) []string { return append([]string{anyOption}, o...) } const ( 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 { i := selected(form, label) if i <= 0 { return "" } return options[i-1] } 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() set(attrFilter{}) }) form.AddButton("Cancel", cancel) form.SetCancelFunc(cancel) arrowNavigation(form) form.SetBorder(true).SetTitle(" Filter by properties ") a.openSidePanel(form, 40, 17, "↑↓ move · ←→ change (applies live) · Enter apply · Esc cancel") }