Add SABnzbd client and Downloads view (M4)
SABnzbd client (queue, history, pause/resume, delete, retry) with lenient number decoding and API-key-safe errors. Downloads view polls while visible, shows queue and history, and supports pause, delete (with confirmation) and retry. Add dev/ with pinned Radarr/Sonarr/SABnzbd containers wired together for manual testing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+32
-4
@@ -3,12 +3,14 @@ package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"git.stinnesbeck.com/nils/arr-tui/internal/config"
|
||||
"git.stinnesbeck.com/nils/arr-tui/internal/radarr"
|
||||
"git.stinnesbeck.com/nils/arr-tui/internal/sabnzbd"
|
||||
"git.stinnesbeck.com/nils/arr-tui/internal/sonarr"
|
||||
)
|
||||
|
||||
@@ -18,6 +20,8 @@ type view struct {
|
||||
key rune
|
||||
name string
|
||||
body tview.Primitive
|
||||
// Optional hooks, called when the view becomes visible / hidden.
|
||||
onShow, onHide func()
|
||||
}
|
||||
|
||||
// App is the top-level arr-tui application.
|
||||
@@ -43,11 +47,15 @@ func New(cfg *config.Config) *App {
|
||||
status: tview.NewTextView().SetDynamicColors(true),
|
||||
}
|
||||
|
||||
downloads := newDownloadsView(a, sabnzbd.NewClient(cfg.SABnzbd.URL, cfg.SABnzbd.APIKey),
|
||||
time.Duration(cfg.UI.RefreshSeconds)*time.Second)
|
||||
radarrClient := radarr.NewClient(cfg.Radarr.URL, cfg.Radarr.APIKey)
|
||||
sonarrClient := sonarr.NewClient(cfg.Sonarr.URL, cfg.Sonarr.APIKey)
|
||||
a.views = []view{
|
||||
{'1', "Movies", newSearchView(a, radarrBackend{radarr.NewClient(cfg.Radarr.URL, cfg.Radarr.APIKey)}, cfg.Radarr).root},
|
||||
{'2', "Series", newSearchView(a, sonarrBackend{sonarr.NewClient(cfg.Sonarr.URL, cfg.Sonarr.APIKey)}, cfg.Sonarr).root},
|
||||
{'3', "Downloads", placeholder("Downloads (M4/M5)")},
|
||||
{'4', "Library", placeholder("Library (v0.2)")},
|
||||
{key: '1', name: "Movies", body: newSearchView(a, radarrBackend{radarrClient}, cfg.Radarr).root},
|
||||
{key: '2', name: "Series", body: newSearchView(a, sonarrBackend{sonarrClient}, cfg.Sonarr).root},
|
||||
{key: '3', name: "Downloads", body: downloads.root, onShow: downloads.activate, onHide: downloads.deactivate},
|
||||
{key: '4', name: "Library", body: placeholder("Library (v0.2)")},
|
||||
}
|
||||
for i, v := range a.views {
|
||||
a.pages.AddPage(v.name, v.body, true, i == 0)
|
||||
@@ -115,6 +123,20 @@ func (a *App) closeModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// confirm asks a yes/no question; yes runs only if the user picks Delete/Yes.
|
||||
func (a *App) confirm(text string, yes func()) {
|
||||
m := tview.NewModal().
|
||||
SetText(text).
|
||||
AddButtons([]string{"Cancel", "Confirm"}). // Cancel first so a stray Enter is safe
|
||||
SetDoneFunc(func(i int, _ string) {
|
||||
a.closeModal()
|
||||
if i == 1 {
|
||||
yes()
|
||||
}
|
||||
})
|
||||
a.openModal(m, 60, 9)
|
||||
}
|
||||
|
||||
// typing reports whether keystrokes belong to a text field or dialog and must
|
||||
// not trigger global shortcuts.
|
||||
func (a *App) typing() bool {
|
||||
@@ -178,6 +200,12 @@ func (a *App) viewShortcut(ev *tcell.EventKey) int {
|
||||
}
|
||||
|
||||
func (a *App) switchTo(i int) {
|
||||
if hide := a.views[a.active].onHide; hide != nil && i != a.active {
|
||||
hide()
|
||||
}
|
||||
if show := a.views[i].onShow; show != nil && i != a.active {
|
||||
show()
|
||||
}
|
||||
a.active = i
|
||||
a.pages.SwitchToPage(a.views[i].name)
|
||||
a.renderTabs()
|
||||
|
||||
Reference in New Issue
Block a user