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, 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"} ) 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} { 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.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 and language. func (a *App) openAttrFilter(cur attrFilter, languages []string, apply 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" 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(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] } form.AddButton("Apply", func() { a.closeModal() apply(attrFilter{ res: pick(lRes, resOptions), codec: pick(lCodec, codecOptions), hdr: pick(lHDR, hdrOptions), lang: pick(lLang, languages), }) }) form.AddButton("Reset", func() { a.closeModal() apply(attrFilter{}) }) form.AddButton("Cancel", a.closeModal) form.SetCancelFunc(a.closeModal) arrowNavigation(form) form.SetBorder(true).SetTitle(" Filter by file properties ") a.openModal(form, 50, 13) }