Add movie search and add flow (M2)

Movies view with debounced Radarr lookup, detail pane, and an add dialog
(quality profile, root folder, monitored, search now). Global shortcuts
are suspended while typing or in a dialog.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 17:42:26 +02:00
co-authored by Claude Sonnet 5.5
parent 2c821fd098
commit 51fb0556b0
6 changed files with 538 additions and 13 deletions
+83
View File
@@ -0,0 +1,83 @@
package ui
import (
"fmt"
"strings"
"github.com/rivo/tview"
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
"git.stinnesbeck.com/nils/arr-tui/internal/radarr"
)
// movieLabel is the one-line list entry for a movie.
func movieLabel(m radarr.Movie) string {
label := tview.Escape(m.Title)
if m.Year > 0 {
label += fmt.Sprintf(" (%d)", m.Year)
}
if m.InLibrary() {
label += " [green]✓[-]"
}
return label
}
// movieDetails renders the detail pane text for a movie.
func movieDetails(m radarr.Movie) string {
var b strings.Builder
title := tview.Escape(m.Title)
if m.Year > 0 {
title += fmt.Sprintf(" (%d)", m.Year)
}
fmt.Fprintf(&b, "[::b]%s[::-]\n", title)
var facts []string
if r := m.Rating(); r > 0 {
facts = append(facts, fmt.Sprintf("★ %.1f", r))
}
if m.Runtime > 0 {
facts = append(facts, fmt.Sprintf("%dh%02dm", m.Runtime/60, m.Runtime%60))
}
if len(m.Genres) > 0 {
facts = append(facts, tview.Escape(strings.Join(m.Genres, ", ")))
}
if len(facts) > 0 {
b.WriteString(strings.Join(facts, " · ") + "\n")
}
if m.Studio != "" {
fmt.Fprintf(&b, "Studio: %s\n", tview.Escape(m.Studio))
}
if m.Status != "" {
fmt.Fprintf(&b, "Status: %s\n", tview.Escape(m.Status))
}
switch {
case m.HasFile:
b.WriteString("[green]In library, downloaded[-]\n")
case m.InLibrary():
b.WriteString("[yellow]In library, not downloaded[-]\n")
}
if m.Overview != "" {
b.WriteString("\n" + tview.Escape(m.Overview) + "\n")
}
return b.String()
}
// profileIndex returns the index of the profile named name, or 0.
func profileIndex(profiles []arr.QualityProfile, name string) int {
for i, p := range profiles {
if strings.EqualFold(p.Name, name) {
return i
}
}
return 0
}
// folderIndex returns the index of the root folder at path, or 0.
func folderIndex(folders []arr.RootFolder, path string) int {
for i, f := range folders {
if strings.TrimRight(f.Path, "/") == strings.TrimRight(path, "/") {
return i
}
}
return 0
}