package ui import ( "context" "strings" "time" "github.com/rivo/tview" "git.stinnesbeck.com/nils/arr-tui/internal/arr" ) // serverChoice is a setting whose values come from the server: a dropdown of // the names Radarr/Sonarr know (quality profiles, root folders), with "(none)" // first. The value in use is kept as an option even when the server does not // list it (or cannot be asked), so nothing is lost by opening the view. type serverChoice struct { *tview.DropDown value string // the selected value; "" = none names []string // from the server, in its order loaded bool // names came from the server quiet func() func() picked func(text string, index int) // SetOptions replaces it, so it is kept here } const noChoice = "(none)" // newServerChoice makes the dropdown. quiet marks changes as not made by the // user while the options are rebuilt; it returns the function that ends that. func newServerChoice(label string, quiet func() func(), changed func()) *serverChoice { c := &serverChoice{DropDown: tview.NewDropDown().SetLabel(label), quiet: quiet} c.picked = func(_ string, i int) { c.value = c.valueAt(i) changed() } c.rebuild() return c } // options lists the values in the order shown: none, the server's names, and // the value in use when the server does not have it. func (c *serverChoice) options() []string { opts := append([]string{""}, c.names...) if c.value != "" && !contains(c.names, c.value) { opts = append(opts, c.value) } return opts } func (c *serverChoice) valueAt(i int) string { opts := c.options() if i < 0 || i >= len(opts) { return "" } return opts[i] } // rebuild puts the options into the dropdown and selects the value. func (c *serverChoice) rebuild() { done := c.quiet() defer done() value := c.value var labels []string sel := 0 for i, o := range c.options() { label := o switch { case o == "": label = noChoice case c.loaded && !contains(c.names, o): label = o + " (not on the server)" } labels = append(labels, label) if o == value { sel = i } } c.value = value c.SetOptions(labels, c.picked) c.SetCurrentOption(sel) // reports sel, which is value } // set selects a value. func (c *serverChoice) set(value string) { c.value = strings.TrimSpace(value) c.rebuild() } // setNames replaces the server's names; nil with loaded false means they are // not known. func (c *serverChoice) setNames(names []string, loaded bool) { c.names, c.loaded = names, loaded c.rebuild() } func contains(list []string, s string) bool { for _, x := range list { if x == s { return true } } return false } // choicesDelay is how long typing in an address or API key pauses before the // server is asked for its profiles and folders. const choicesDelay = 700 * time.Millisecond // scheduleChoices asks the service again once its address and key have not // changed for choicesDelay. func (v *settingsView) scheduleChoices(service string) { if t := v.choiceTimers[service]; t != nil { t.Stop() } v.choiceTimers[service] = time.AfterFunc(choicesDelay, func() { v.app.post(func() { v.loadChoices(service) }) }) } // loadChoices fills the profile and folder dropdowns of a service ("radarr" or // "sonarr") from the server, using the address and key typed in the fields. func (v *settingsView) loadChoices(service string) { profile, folder := v.choices[service+".profile"], v.choices[service+".folder"] url := strings.TrimSpace(v.inputs[service+".url"].GetText()) key := strings.TrimSpace(v.inputs[service+".apikey"].GetText()) v.choiceGen[service]++ gen := v.choiceGen[service] if url == "" || key == "" { profile.setNames(nil, false) folder.setNames(nil, false) return } go func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() profiles, folders, err := fetchChoices(ctx, url, key) v.app.post(func() { v.applyChoices(service, gen, profiles, folders, err) }) }() } // fetchChoices asks a Radarr/Sonarr for its quality profile names and root // folder paths. func fetchChoices(ctx context.Context, url, key string) (profiles, folders []string, err error) { client := arr.NewClient(url, key) qp, err := client.QualityProfiles(ctx) if err != nil { return nil, nil, err } rf, err := client.RootFolders(ctx) if err != nil { return nil, nil, err } for _, p := range qp { profiles = append(profiles, p.Name) } for _, f := range rf { folders = append(folders, f.Path) } return profiles, folders, nil } // applyChoices puts what fetchChoices found into the dropdowns, unless the // address or key changed since it was asked (gen). func (v *settingsView) applyChoices(service string, gen int, profiles, folders []string, err error) { if v.choiceGen[service] != gen { return } profile, folder := v.choices[service+".profile"], v.choices[service+".folder"] if err != nil { profile.setNames(nil, false) folder.setNames(nil, false) name := map[string]string{"radarr": "Radarr", "sonarr": "Sonarr"}[service] if msg := err.Error(); v.choiceErr[service] != msg { v.choiceErr[service] = msg v.report("[yellow]" + name + ": quality profiles and root folders not loaded: " + tview.Escape(msg) + "[-]") } return } delete(v.choiceErr, service) profile.setNames(profiles, true) folder.setNames(folders, true) }