Detail panes: path in the head, text flows around the poster
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-2
@@ -16,8 +16,7 @@ First release.
|
|||||||
- **Series drill-down:** seasons and episodes with monitoring, automatic search and release search.
|
- **Series drill-down:** seasons and episodes with monitoring, automatic search and release search.
|
||||||
- **Downloads:** SABnzbd queue and history plus the Radarr and Sonarr queues; pause/resume, delete,
|
- **Downloads:** SABnzbd queue and history plus the Radarr and Sonarr queues; pause/resume, delete,
|
||||||
retry, remove and blocklist.
|
retry, remove and blocklist.
|
||||||
- **Posters:** a thumbnail at the top right of the detail panes, title and facts to its left, plot
|
- **Posters:** a thumbnail at the top right of the detail panes with the text flowing around it (quadrant or, on capable terminals, sextant block rendering; `ui.cover_style`).
|
||||||
below (quadrant or, on capable terminals, sextant block rendering; `ui.cover_style`).
|
|
||||||
- **Log (F5) and popups:** feedback appears as fading popups; the log keeps the history.
|
- **Log (F5) and popups:** feedback appears as fading popups; the log keeps the history.
|
||||||
- Connection indicators for all three services, context-sensitive key hints, `?` help.
|
- Connection indicators for all three services, context-sensitive key hints, `?` help.
|
||||||
- JSON config with environment overrides (`ARR_TUI_*`), `--version`, cross-platform builds
|
- JSON config with environment overrides (`ARR_TUI_*`), `--version`, cross-platform builds
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ A terminal UI to search for movies and series with [Radarr](https://radarr.video
|
|||||||
optionally start the automatic search.
|
optionally start the automatic search.
|
||||||
- **Interactive search**: list the releases Radarr/Sonarr find (approved or rejected, with reasons),
|
- **Interactive search**: list the releases Radarr/Sonarr find (approved or rejected, with reasons),
|
||||||
sort and filter them (by approval, quality, x265 / x264 / AV1, HDR, Dolby Vision, audio format, language), and grab one. Series: pick a season or a single episode first.
|
sort and filter them (by approval, quality, x265 / x264 / AV1, HDR, Dolby Vision, audio format, language), and grab one. Series: pick a season or a single episode first.
|
||||||
- **Posters**: the detail panes show the poster at the top right, with title and facts to its left
|
- **Posters**: the detail panes show the poster at the top right; title, facts, path and plot
|
||||||
and the plot below. It is drawn with colored block characters (works over SSH, needs a terminal
|
flow along its left side and continue at full width below it. It is drawn with colored block characters (works over SSH, needs a terminal
|
||||||
with colors). Library items use the copy stored in Radarr/Sonarr; search results are downloaded
|
with colors). Library items use the copy stored in Radarr/Sonarr; search results are downloaded
|
||||||
from the metadata provider's image host (TMDb, TheTVDB). Turn it off with
|
from the metadata provider's image host (TMDb, TheTVDB). Turn it off with
|
||||||
`"ui": {"covers": false}`. `"ui": {"cover_style": ...}` picks the characters: `quadrant` (2x2
|
`"ui": {"covers": false}`. `"ui": {"cover_style": ...}` picks the characters: `quadrant` (2x2
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Notes:
|
|||||||
- Radarr and Sonarr queues (status, warnings, import errors), so stuck imports are visible.
|
- Radarr and Sonarr queues (status, warnings, import errors), so stuck imports are visible.
|
||||||
- Global speed / pause state in the status bar. Pause/resume and delete a SABnzbd job.
|
- Global speed / pause state in the status bar. Pause/resume and delete a SABnzbd job.
|
||||||
- **Status bar / help:** the bottom bar lists the keys that work in the current view or panel; the top right shows the connection state of each service; `?` opens the full key reference.
|
- **Status bar / help:** the bottom bar lists the keys that work in the current view or panel; the top right shows the connection state of each service; `?` opens the full key reference.
|
||||||
- **Posters:** the detail panes show the poster at the top right, the title and facts to its left and the plot below both; on panes too small for that, everything is one text. The poster is drawn with block characters in 24-bit colors: every cell shows several pixels in the best two-color split (`quadrant`: 2×2 pixels, glyphs from the Block Elements range every font has; `sextant`: 2×3 pixels, sharper, glyphs from Symbols for Legacy Computing). `ui.cover_style` is `auto` (default: sextant on kitty, WezTerm, foot, Ghostty, quadrant elsewhere), `quadrant` or `sextant`. JPEG/PNG only, decoded and downscaled in the process, so no terminal graphics protocol is needed. Library items fetch `/MediaCover/<id>/poster-250.jpg` from their Radarr/Sonarr instance with the API key; search results fall back to the provider's `remoteUrl` (fetched without credentials). Posters are cached in memory, failed downloads are not retried for two minutes, and the poster shrinks or disappears when the pane is too short. `ui.covers: false` disables it.
|
- **Posters:** the detail panes show the poster at the top right; the head (title, facts, path) and the plot are one text that flows along the poster's left side and continues at full width below it (text is wrapped by arr-tui itself, with a narrower width for the first lines); on panes too small for the poster, the text simply uses the full width. The poster is drawn with block characters in 24-bit colors: every cell shows several pixels in the best two-color split (`quadrant`: 2×2 pixels, glyphs from the Block Elements range every font has; `sextant`: 2×3 pixels, sharper, glyphs from Symbols for Legacy Computing). `ui.cover_style` is `auto` (default: sextant on kitty, WezTerm, foot, Ghostty, quadrant elsewhere), `quadrant` or `sextant`. JPEG/PNG only, decoded and downscaled in the process, so no terminal graphics protocol is needed. Library items fetch `/MediaCover/<id>/poster-250.jpg` from their Radarr/Sonarr instance with the API key; search results fall back to the provider's `remoteUrl` (fetched without credentials). Posters are cached in memory, failed downloads are not retried for two minutes, and the poster shrinks or disappears when the pane is too short. `ui.covers: false` disables it.
|
||||||
- **Notifications and log (F5):** feedback about actions (added, deleted, grabbed, failures) appears as popups in the bottom right that fade away (errors stay longer). Everything is also kept in the Log view; only connection changes of the background polling are logged, not every failed poll.
|
- **Notifications and log (F5):** feedback about actions (added, deleted, grabbed, failures) appears as popups in the bottom right that fade away (errors stay longer). Everything is also kept in the Log view; only connection changes of the background polling are logged, not every failed poll.
|
||||||
|
|
||||||
### 4.2 v0.2 (done, M8)
|
### 4.2 v0.2 (done, M8)
|
||||||
|
|||||||
+82
-43
@@ -2,6 +2,7 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
"github.com/rivo/tview"
|
"github.com/rivo/tview"
|
||||||
@@ -9,48 +10,48 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
minCoverRows = 6 // a smaller poster is not worth showing
|
minCoverRows = 6 // a smaller poster is not worth showing
|
||||||
minCoverCols = 8 //
|
minCoverCols = 8
|
||||||
minTextRows = 10 // the poster never takes these rows from the text
|
minTextRows = 6 // the poster never takes these rows from the text
|
||||||
)
|
)
|
||||||
|
|
||||||
// detailPane shows the details of one title: the head (title and facts) with
|
// detailPane shows the details of one title as one flowing text with the
|
||||||
// the poster to its right on top, and the plot below both. On panes too small
|
// poster floating at the top right: the head (title and facts) and the plot
|
||||||
// for the poster everything is one text.
|
// run along the poster on the left and continue at full width below it. On
|
||||||
|
// panes too small for the poster the text simply uses the full width.
|
||||||
type detailPane struct {
|
type detailPane struct {
|
||||||
*tview.Flex // column: top row, plot
|
*tview.Flex // holds the text; the poster is drawn on top of its corner
|
||||||
app *App
|
app *App
|
||||||
top *tview.Flex // row: head, cover
|
text *tview.TextView
|
||||||
head *tview.TextView
|
|
||||||
cover *coverView
|
cover *coverView
|
||||||
plot *tview.TextView
|
|
||||||
|
|
||||||
text detailText
|
detail detailText
|
||||||
want string // key of the poster that should be shown
|
want string // key of the poster that should be shown
|
||||||
hasCover bool // a poster (or its loading note) is part of the content
|
hasCover bool // a poster (or its loading note) is part of the content
|
||||||
combined bool // head and plot are shown together in the plot view
|
|
||||||
|
// What the text was last wrapped for.
|
||||||
|
laidOut struct {
|
||||||
|
width, narrow, rows int
|
||||||
|
valid bool
|
||||||
|
}
|
||||||
|
coverW, coverH int // size of the poster on screen, 0 = hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDetailPane(app *App) *detailPane {
|
func newDetailPane(app *App) *detailPane {
|
||||||
d := &detailPane{
|
d := &detailPane{
|
||||||
Flex: tview.NewFlex().SetDirection(tview.FlexRow),
|
Flex: tview.NewFlex(),
|
||||||
app: app,
|
app: app,
|
||||||
top: tview.NewFlex(),
|
text: tview.NewTextView().SetDynamicColors(true).SetWrap(false),
|
||||||
head: tview.NewTextView().SetDynamicColors(true).SetWordWrap(true),
|
|
||||||
cover: newCoverView(app.coverStyle),
|
cover: newCoverView(app.coverStyle),
|
||||||
plot: tview.NewTextView().SetDynamicColors(true).SetWordWrap(true),
|
|
||||||
}
|
}
|
||||||
d.cover.SetBorderPadding(0, 0, 1, 0)
|
d.AddItem(d.text, 0, 1, false)
|
||||||
d.top.AddItem(d.head, 0, 1, false).AddItem(d.cover, 0, 0, false)
|
|
||||||
d.AddItem(d.top, 0, 0, false).AddItem(d.plot, 0, 1, false)
|
|
||||||
d.SetBorder(true).SetTitle(" Details ")
|
d.SetBorder(true).SetTitle(" Details ")
|
||||||
d.combined = true
|
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show displays the text and, if there is one, the poster.
|
// Show displays the text and, if there is one, the poster.
|
||||||
func (d *detailPane) Show(text detailText, ref coverRef) {
|
func (d *detailPane) Show(text detailText, ref coverRef) {
|
||||||
d.text = text
|
d.detail = text
|
||||||
d.applyText()
|
d.laidOut.valid = false
|
||||||
if !d.app.covers || ref.fetch == nil {
|
if !d.app.covers || ref.fetch == nil {
|
||||||
d.want, d.hasCover = "", false
|
d.want, d.hasCover = "", false
|
||||||
d.cover.setImage(nil)
|
d.cover.setImage(nil)
|
||||||
@@ -75,33 +76,71 @@ func (d *detailPane) Show(text detailText, ref coverRef) {
|
|||||||
// Clear empties the pane.
|
// Clear empties the pane.
|
||||||
func (d *detailPane) Clear() { d.Show(detailText{}, coverRef{}) }
|
func (d *detailPane) Clear() { d.Show(detailText{}, coverRef{}) }
|
||||||
|
|
||||||
// applyText puts the texts into their views for the current layout.
|
|
||||||
func (d *detailPane) applyText() {
|
|
||||||
if d.combined {
|
|
||||||
d.head.SetText("")
|
|
||||||
d.plot.SetText(d.text.String()).ScrollToBeginning()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.head.SetText(d.text.head).ScrollToBeginning()
|
|
||||||
d.plot.SetText(d.text.plot).ScrollToBeginning()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *detailPane) Draw(screen tcell.Screen) {
|
func (d *detailPane) Draw(screen tcell.Screen) {
|
||||||
_, _, w, h := d.GetInnerRect()
|
x, y, w, h := d.GetInnerRect()
|
||||||
coverW, coverH := 0, 0
|
|
||||||
|
d.coverW, d.coverH = 0, 0
|
||||||
if d.hasCover {
|
if d.hasCover {
|
||||||
aw, ah := d.cover.aspect()
|
aw, ah := d.cover.aspect()
|
||||||
// At most 40% of the width, and the plot keeps its rows.
|
// At most 40% of the width, and the text keeps some rows below.
|
||||||
coverW, coverH = coverCells(aw, ah, w*2/5, h-minTextRows)
|
d.coverW, d.coverH = coverCells(aw, ah, w*2/5, h-minTextRows)
|
||||||
if coverH < minCoverRows || coverW < minCoverCols {
|
if d.coverH < minCoverRows || d.coverW < minCoverCols {
|
||||||
coverW, coverH = 0, 0
|
d.coverW, d.coverH = 0, 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if combined := coverW == 0; combined != d.combined {
|
|
||||||
d.combined = combined
|
narrow, rows := w, 0
|
||||||
d.applyText()
|
if d.coverW > 0 {
|
||||||
|
narrow, rows = w-d.coverW-1, d.coverH // one column of space left of the poster
|
||||||
}
|
}
|
||||||
d.ResizeItem(d.top, coverH, 0)
|
if !d.laidOut.valid || d.laidOut.width != w || d.laidOut.narrow != narrow || d.laidOut.rows != rows {
|
||||||
d.top.ResizeItem(d.cover, coverW+1, 0) // +1: the padding left of the poster
|
d.laidOut.width, d.laidOut.narrow, d.laidOut.rows, d.laidOut.valid = w, narrow, rows, true
|
||||||
|
d.text.SetText(strings.Join(flowWrap(d.detail.String(), narrow, w, rows), "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
d.Flex.Draw(screen)
|
d.Flex.Draw(screen)
|
||||||
|
if d.coverW > 0 {
|
||||||
|
d.cover.SetRect(x+w-d.coverW, y, d.coverW, d.coverH)
|
||||||
|
d.cover.Draw(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// flowWrap word-wraps text for a column that is narrow for its first
|
||||||
|
// narrowRows lines and wide afterwards, like text flowing around a picture
|
||||||
|
// floating at the top right. Newlines in the text are kept. Color tags have
|
||||||
|
// no width and carry over from line to line.
|
||||||
|
func flowWrap(text string, narrow, wide, narrowRows int) []string {
|
||||||
|
var lines []string
|
||||||
|
var cur strings.Builder
|
||||||
|
curW := 0
|
||||||
|
limit := func() int {
|
||||||
|
if len(lines) < narrowRows {
|
||||||
|
return max(narrow, 1)
|
||||||
|
}
|
||||||
|
return max(wide, 1)
|
||||||
|
}
|
||||||
|
flush := func() {
|
||||||
|
lines = append(lines, cur.String())
|
||||||
|
cur.Reset()
|
||||||
|
curW = 0
|
||||||
|
}
|
||||||
|
for _, para := range strings.Split(text, "\n") {
|
||||||
|
for _, word := range strings.Fields(para) {
|
||||||
|
ww := tview.TaggedStringWidth(word)
|
||||||
|
if curW > 0 && curW+1+ww > limit() {
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
if curW > 0 {
|
||||||
|
cur.WriteByte(' ')
|
||||||
|
curW++
|
||||||
|
}
|
||||||
|
cur.WriteString(word)
|
||||||
|
curW += ww
|
||||||
|
}
|
||||||
|
flush() // end of paragraph
|
||||||
|
}
|
||||||
|
for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
|
||||||
|
lines = lines[:len(lines)-1]
|
||||||
|
}
|
||||||
|
return lines
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-25
@@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/rivo/tview"
|
||||||
)
|
)
|
||||||
|
|
||||||
func rowText(screen tcell.SimulationScreen, y, x0, x1 int) string {
|
func rowText(screen tcell.SimulationScreen, y, x0, x1 int) string {
|
||||||
@@ -16,7 +17,38 @@ func rowText(screen tcell.SimulationScreen, y, x0, x1 int) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetailPaneLayout(t *testing.T) {
|
func TestFlowWrap(t *testing.T) {
|
||||||
|
text := "one two three four five six seven eight nine ten"
|
||||||
|
// Narrow (10) for the first two lines, wide (25) afterwards.
|
||||||
|
got := flowWrap(text, 10, 25, 2)
|
||||||
|
want := []string{"one two", "three four", "five six seven eight nine", "ten"}
|
||||||
|
if strings.Join(got, "|") != strings.Join(want, "|") {
|
||||||
|
t.Errorf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
// Without a poster everything is wide.
|
||||||
|
if got := flowWrap(text, 10, 25, 0); got[0] != "one two three four five" {
|
||||||
|
t.Errorf("no poster: %q", got)
|
||||||
|
}
|
||||||
|
// Newlines are kept, blank lines too; trailing blank lines are dropped.
|
||||||
|
got = flowWrap("head\nfacts\n\nplot words\n", 20, 20, 0)
|
||||||
|
if strings.Join(got, "|") != "head|facts||plot words" {
|
||||||
|
t.Errorf("newlines: %q", got)
|
||||||
|
}
|
||||||
|
// Color tags have no width and escaped brackets count as one character.
|
||||||
|
got = flowWrap("[red]ab[-] cd ef", 5, 5, 0)
|
||||||
|
if strings.Join(got, "|") != "[red]ab[-] cd|ef" {
|
||||||
|
t.Errorf("tags: %q", got)
|
||||||
|
}
|
||||||
|
if got := flowWrap(tview.Escape("[1080p]")+" x", 9, 9, 0); len(got) != 1 {
|
||||||
|
t.Errorf("escaped brackets: %q", got)
|
||||||
|
}
|
||||||
|
// A word wider than the column is not split or lost.
|
||||||
|
if got := flowWrap("a verylongwordhere b", 5, 5, 0); strings.Join(got, "|") != "a|verylongwordhere|b" {
|
||||||
|
t.Errorf("long word: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetailPaneFlowsAroundPoster(t *testing.T) {
|
||||||
a := newTestApp(t)
|
a := newTestApp(t)
|
||||||
screen := tcell.NewSimulationScreen("")
|
screen := tcell.NewSimulationScreen("")
|
||||||
if err := screen.Init(); err != nil {
|
if err := screen.Init(); err != nil {
|
||||||
@@ -25,52 +57,51 @@ func TestDetailPaneLayout(t *testing.T) {
|
|||||||
defer screen.Fini()
|
defer screen.Fini()
|
||||||
screen.SetSize(60, 40)
|
screen.SetSize(60, 40)
|
||||||
|
|
||||||
|
plot := strings.Repeat("plot ", 220) // long enough to run past the poster
|
||||||
d := newDetailPane(a)
|
d := newDetailPane(a)
|
||||||
d.SetRect(0, 0, 60, 40)
|
d.SetRect(0, 0, 60, 40)
|
||||||
d.Show(detailText{head: "[::b]The Matrix (1999)[::-]\nStatus: released", plot: "Plot text goes here"}, coverRef{})
|
d.Show(detailText{head: "[::b]The Matrix (1999)[::-]\nStatus: released\n", plot: plot}, coverRef{})
|
||||||
|
|
||||||
// Without a poster everything is one text.
|
// Without a poster the text uses the full width from the top.
|
||||||
d.Draw(screen)
|
d.Draw(screen)
|
||||||
if got := rowText(screen, 1, 1, 30); !strings.HasPrefix(got, "The Matrix (1999)") {
|
if got := rowText(screen, 1, 1, 30); !strings.HasPrefix(got, "The Matrix (1999)") {
|
||||||
t.Errorf("first line = %q", got)
|
t.Errorf("first line = %q", got)
|
||||||
}
|
}
|
||||||
if got := rowText(screen, 3, 1, 30); !strings.HasPrefix(got, "Plot text goes here") {
|
if got := strings.TrimSpace(rowText(screen, 5, 1, 59)); len(got) < 50 {
|
||||||
t.Errorf("without a poster the plot follows the head: %q", got)
|
t.Errorf("without a poster the text should use the full width: %q", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
// With a poster: head on the left, poster on the right, plot below both.
|
// With a poster (23×17 cells at the top right) the text runs beside it.
|
||||||
d.hasCover = true
|
d.hasCover = true
|
||||||
d.cover.setImage(stripes(40, 60))
|
d.cover.setImage(stripes(40, 60))
|
||||||
d.Draw(screen)
|
d.Draw(screen)
|
||||||
|
if d.coverW != 23 || d.coverH != 17 {
|
||||||
|
t.Fatalf("poster size %dx%d, want 23x17", d.coverW, d.coverH)
|
||||||
|
}
|
||||||
if got := rowText(screen, 1, 1, 30); !strings.HasPrefix(got, "The Matrix (1999)") {
|
if got := rowText(screen, 1, 1, 30); !strings.HasPrefix(got, "The Matrix (1999)") {
|
||||||
t.Errorf("head moved: %q", got)
|
t.Errorf("head moved: %q", got)
|
||||||
}
|
}
|
||||||
coverX := -1
|
if got := rowText(screen, 4, 1, 30); !strings.HasPrefix(got, "plot plot") {
|
||||||
for x := 59; x > 30; x-- {
|
t.Errorf("the plot starts beside the poster, right after the head: %q", got)
|
||||||
if ch, _, st, _ := screen.GetContent(x, 1); ch != ' ' || st != tcell.StyleDefault {
|
}
|
||||||
coverX = x
|
// Beside the poster the text stays left of it (34 columns), below it it uses the full width.
|
||||||
break
|
for y := 1; y <= d.coverH; y++ {
|
||||||
|
if got := strings.TrimRight(rowText(screen, y, 1, 35), " "); len([]rune(got)) > 34 {
|
||||||
|
t.Errorf("row %d runs into the poster: %q", y, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if coverX < 55 {
|
if got := strings.TrimSpace(rowText(screen, d.coverH+3, 1, 59)); len(got) < 50 {
|
||||||
t.Errorf("no poster against the right edge (last drawn column %d)", coverX)
|
t.Errorf("below the poster the text should use the full width: %q", got)
|
||||||
}
|
}
|
||||||
// The plot starts below the poster: 40*2/5 = 24 columns -> 24*3/2/2 = 18 rows.
|
// The poster sits against the right edge of the pane.
|
||||||
plotRow := -1
|
if ch, _, st, _ := screen.GetContent(58, 1); ch == ' ' && st == tcell.StyleDefault {
|
||||||
for y := 1; y < 39; y++ {
|
t.Error("nothing drawn at the poster's right edge")
|
||||||
if strings.HasPrefix(rowText(screen, y, 1, 30), "Plot text goes here") {
|
|
||||||
plotRow = y
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if plotRow < 18 {
|
|
||||||
t.Errorf("plot at row %d, want below the poster", plotRow)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A pane too short for the poster falls back to plain text.
|
// A pane too short for the poster falls back to plain text.
|
||||||
d.SetRect(0, 0, 60, 14)
|
d.SetRect(0, 0, 60, 10)
|
||||||
d.Draw(screen)
|
d.Draw(screen)
|
||||||
if !d.combined {
|
if d.coverW != 0 {
|
||||||
t.Error("a short pane must drop the poster")
|
t.Error("a short pane must drop the poster")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,10 +106,7 @@ func newMovieItem(m radarr.Movie) libItem {
|
|||||||
it.status = "[yellow]missing"
|
it.status = "[yellow]missing"
|
||||||
}
|
}
|
||||||
it.details = movieDetails(m)
|
it.details = movieDetails(m)
|
||||||
it.details.head += monitoredLine(m.Monitored) + sizeLine(m.SizeOnDisk)
|
it.details.head += monitoredLine(m.Monitored) + sizeLine(m.SizeOnDisk) + pathLine(m.Path)
|
||||||
if path := pathLine(m.Path); path != "" {
|
|
||||||
it.details.plot += "\n\n" + path
|
|
||||||
}
|
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user