feat: show YouTube videos as two-line entries

This commit is contained in:
2026-10-18 10:48:52 +02:00
parent 3e701c99c9
commit 493edfcf3a
3 changed files with 47 additions and 30 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ Startup shows a centered **YouTube / Miruro** selector. Use `j` / `k` or arrows
After selection, the browser starts in **normal mode**. Press `i` to type a search; Enter submits a changed query and returns to normal mode. Esc returns without submitting. After selection, the browser starts in **normal mode**. Press `i` to type a search; Enter submits a changed query and returns to normal mode. Esc returns without submitting.
Both providers use the same layout: a cyan view heading, bordered search and results, a bordered status line, and a dim one-line key hint bar. The selected row uses black text on cyan with a `> ` marker; saved titles are green, watch-later titles magenta, and `✓` marks watched entries. Metadata and help open in centered cyan-bordered popups; bulk-removal confirmations use yellow borders. Miruro keeps its catalog navigation and two-line title/episode rows rather than YouTube's video table. Both providers use the same layout: a cyan view heading, bordered search and results, a bordered status line, and a dim one-line key hint bar. Entries use two lines: the title above dimmed metadata. YouTube shows the channel and publication date beneath each video title, without separate channel/date columns; tagged channels remain cyan. The selected row uses black text on cyan with a `> ` marker; saved titles are green, watch-later titles magenta, and `✓` marks watched entries. Metadata and help open in centered cyan-bordered popups; bulk-removal confirmations use yellow borders. Miruro keeps its catalog navigation and title/episode metadata.
Selecting a YouTube video launches: Selecting a YouTube video launches:
+21 -13
View File
@@ -101,7 +101,7 @@
//! Switching views cancels in-flight extraction; returning retains the unaccepted request and per-channel offsets. //! Switching views cancels in-flight extraction; returning retains the unaccepted request and per-channel offsets.
//! Errors keep loaded rows and stop automatic retries; r reloads config and restarts the current view. //! Errors keep loaded rows and stop automatic retries; r reloads config and restarts the current view.
//! Deep pages may slow down because yt-dlp offsets re-traverse earlier continuation pages. //! Deep pages may slow down because yt-dlp offsets re-traverse earlier continuation pages.
//! Video tables show Published as YYYY-MM-DD; ~ marks estimated UTC dates, Unknown missing metadata. //! Video entries show the title above channel · YYYY-MM-DD; ~ marks estimated UTC dates, Unknown missing metadata.
//! Prefer exact upload dates and cache date labels during extraction, not on every redraw. //! Prefer exact upload dates and cache date labels during extraction, not on every redraw.
//! Terebi receives `--new-instance --url` and a private `--progress-file`; each selection gets its own player. //! Terebi receives `--new-instance --url` and a private `--progress-file`; each selection gets its own player.
//! `--player` overrides its CLI path; otherwise `terebi` is resolved through PATH. //! `--player` overrides its CLI path; otherwise `terebi` is resolved through PATH.
@@ -2986,39 +2986,47 @@ impl App {
.categories .categories
.values() .values()
.any(|channels| video_in_channels(video, channels, &self.config)); .any(|channels| video_in_channels(video, channels, &self.config));
let channel = Cell::from(video.channel.as_deref().unwrap_or("Unknown")); let title = Span::raw(video.title.as_str());
let channel = Span::raw(video.channel.as_deref().unwrap_or("Unknown"));
Row::new([ Row::new([
Cell::from(if self.video_state.is_watched(&video.id) { Cell::from(if self.video_state.is_watched(&video.id) {
"✓" "✓"
} else { } else {
"" ""
}), }),
Cell::from(Text::from(vec![
Line::from(
if self.video_state.contains(CollectionKind::Saved, &video.id) { if self.video_state.contains(CollectionKind::Saved, &video.id) {
Cell::from(video.title.as_str()).fg(Color::Green) title.fg(Color::Green)
} else if self } else if self
.video_state .video_state
.contains(CollectionKind::WatchLater, &video.id) .contains(CollectionKind::WatchLater, &video.id)
{ {
Cell::from(video.title.as_str()).fg(Color::Magenta) title.fg(Color::Magenta)
} else { } else {
Cell::from(video.title.as_str()) title
}, },
),
Line::from(vec![
if tagged { if tagged {
channel.fg(Color::Cyan) channel.fg(Color::Cyan)
} else { } else {
channel channel
}, },
Cell::from(video.publication_date.as_deref().unwrap_or("Unknown")), " · ".into(),
video
.publication_date
.as_deref()
.unwrap_or("Unknown")
.into(),
]) ])
.dim(),
])),
])
.height(2)
}), }),
[ [Constraint::Length(1), Constraint::Fill(1)],
Constraint::Length(1),
Constraint::Fill(3),
Constraint::Fill(1),
Constraint::Length(11),
],
) )
.header(Row::new(["", "Video", "Channel", "Published"]).bold())
.block(Block::bordered().title(loading_title( .block(Block::bordered().title(loading_title(
if view.source == ViewSource::Search { if view.source == ViewSource::Search {
" Results " " Results "
+13 -4
View File
@@ -3010,9 +3010,9 @@ fn video_channel_colors_follow_category_membership_and_preserve_selection_contra
.draw(|frame| app.draw(frame, Duration::ZERO)) .draw(|frame| app.draw(frame, Duration::ZERO))
.unwrap(); .unwrap();
let buffer = terminal.backend().buffer(); let buffer = terminal.backend().buffer();
// One tab row, three search rows, then the table border and header. // One tab row, three search rows, then the results border and two-line entries.
for (index, (label, _, _, tagged)) in cases.iter().enumerate() { for (index, (label, _, _, tagged)) in cases.iter().enumerate() {
let y = 6 + index as u16; let y = 6 + 2 * index as u16;
let row: String = (0..120).map(|x| buffer[(x, y)].symbol()).collect(); let row: String = (0..120).map(|x| buffer[(x, y)].symbol()).collect();
let x = row.find(label).expect("channel label is visible") as u16; let x = row.find(label).expect("channel label is visible") as u16;
assert_eq!( assert_eq!(
@@ -3024,8 +3024,17 @@ fn video_channel_colors_follow_category_membership_and_preserve_selection_contra
}, },
"{label} in row {index}, cleared={cleared}" "{label} in row {index}, cleared={cleared}"
); );
assert_eq!(buffer[(3, y)].fg, Color::Reset, "title stays uncolored"); let title: String = (0..120).map(|x| buffer[(x, y - 1)].symbol()).collect();
assert_eq!(buffer[(108, y)].fg, Color::Reset, "date stays uncolored"); assert!(title.contains("Video title"));
let date_x = row.find("2026-10-11").expect("date follows channel") as u16;
assert!(row.contains(&format!("{label} · 2026-10-11")));
assert_eq!(buffer[(3, y - 1)].fg, Color::Reset, "title stays uncolored");
assert_eq!(buffer[(date_x, y)].fg, Color::Reset, "date stays uncolored");
assert!(
buffer[(x, y)]
.modifier
.contains(ratatui::style::Modifier::DIM)
);
} }
if !cleared { if !cleared {
app.views[0].selection.select(Some(0)); app.views[0].selection.select(Some(0));