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:
+70
-9
@@ -6,8 +6,12 @@ import (
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.stinnesbeck.com/nils/arr-tui/internal/config"
|
||||
)
|
||||
|
||||
const modalPage = "modal"
|
||||
|
||||
type view struct {
|
||||
key rune
|
||||
name string
|
||||
@@ -22,10 +26,14 @@ type App struct {
|
||||
status *tview.TextView
|
||||
views []view
|
||||
active int
|
||||
|
||||
message string
|
||||
modalOpen bool
|
||||
modalFocus tview.Primitive
|
||||
}
|
||||
|
||||
// New builds the application shell: a tab bar, one page per view and a status bar.
|
||||
func New() *App {
|
||||
func New(cfg *config.Config) *App {
|
||||
a := &App{
|
||||
app: tview.NewApplication(),
|
||||
tabs: tview.NewTextView().SetDynamicColors(true),
|
||||
@@ -34,7 +42,7 @@ func New() *App {
|
||||
}
|
||||
|
||||
a.views = []view{
|
||||
{'1', "Movies", placeholder("Movie search (M2)")},
|
||||
{'1', "Movies", newMoviesView(a, cfg.Radarr).root},
|
||||
{'2', "Series", placeholder("Series search (M3)")},
|
||||
{'3', "Downloads", placeholder("Downloads (M4/M5)")},
|
||||
{'4', "Library", placeholder("Library (v0.2)")},
|
||||
@@ -50,15 +58,21 @@ func New() *App {
|
||||
|
||||
a.app.SetRoot(layout, true).EnableMouse(false)
|
||||
a.app.SetInputCapture(a.handleKey)
|
||||
a.SetStatus("[gray]q quit · Tab / 1-4 switch view")
|
||||
a.switchTo(0)
|
||||
a.renderStatus()
|
||||
return a
|
||||
}
|
||||
|
||||
// SetStatus replaces the status bar text. Safe to call only from the UI goroutine;
|
||||
// use QueueUpdateDraw from other goroutines.
|
||||
func (a *App) SetStatus(text string) {
|
||||
a.status.SetText(text)
|
||||
// SetMessage shows a message in the status bar. UI goroutine only; use
|
||||
// post from other goroutines.
|
||||
func (a *App) SetMessage(text string) {
|
||||
a.message = text
|
||||
a.renderStatus()
|
||||
}
|
||||
|
||||
// post runs f on the UI goroutine and redraws. Safe to call from any goroutine.
|
||||
func (a *App) post(f func()) {
|
||||
a.app.QueueUpdateDraw(f)
|
||||
}
|
||||
|
||||
// Run starts the event loop and blocks until the application quits.
|
||||
@@ -66,11 +80,58 @@ func (a *App) Run() error {
|
||||
return a.app.Run()
|
||||
}
|
||||
|
||||
func (a *App) renderStatus() {
|
||||
hints := "[gray]q quit · Tab/1-4 views"
|
||||
if a.message == "" {
|
||||
a.status.SetText(hints)
|
||||
return
|
||||
}
|
||||
a.status.SetText(a.message + " [gray]│ " + hints)
|
||||
}
|
||||
|
||||
// openModal shows p centered over the current view and remembers the focus.
|
||||
func (a *App) openModal(p tview.Primitive, width, height int) {
|
||||
a.modalFocus = a.app.GetFocus()
|
||||
a.modalOpen = true
|
||||
box := tview.NewFlex().
|
||||
AddItem(nil, 0, 1, false).
|
||||
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(nil, 0, 1, false).
|
||||
AddItem(p, height, 0, true).
|
||||
AddItem(nil, 0, 1, false), width, 0, true).
|
||||
AddItem(nil, 0, 1, false)
|
||||
a.pages.AddPage(modalPage, box, true, true)
|
||||
a.app.SetFocus(p)
|
||||
}
|
||||
|
||||
func (a *App) closeModal() {
|
||||
a.pages.RemovePage(modalPage)
|
||||
a.modalOpen = false
|
||||
if a.modalFocus != nil {
|
||||
a.app.SetFocus(a.modalFocus)
|
||||
a.modalFocus = nil
|
||||
}
|
||||
}
|
||||
|
||||
// typing reports whether keystrokes belong to a text field or dialog and must
|
||||
// not trigger global shortcuts.
|
||||
func (a *App) typing() bool {
|
||||
if a.modalOpen {
|
||||
return true
|
||||
}
|
||||
_, isInput := a.app.GetFocus().(*tview.InputField)
|
||||
return isInput
|
||||
}
|
||||
|
||||
func (a *App) handleKey(ev *tcell.EventKey) *tcell.EventKey {
|
||||
switch ev.Key() {
|
||||
case tcell.KeyCtrlC:
|
||||
if ev.Key() == tcell.KeyCtrlC {
|
||||
a.app.Stop()
|
||||
return nil
|
||||
}
|
||||
if a.typing() {
|
||||
return ev
|
||||
}
|
||||
switch ev.Key() {
|
||||
case tcell.KeyTab:
|
||||
a.switchTo((a.active + 1) % len(a.views))
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user