The two defaults of Radarr and Sonarr are dropdowns filled from the server with the address and key in the fields: when the view opens, on Revert, and shortly after either is changed. A configured value the server does not list stays selectable, marked "(not on the server)". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
183 lines
5.2 KiB
Go
183 lines
5.2 KiB
Go
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)
|
|
}
|