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.
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:
+21 -13
View File
@@ -101,7 +101,7 @@
//! 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.
//! 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.
//! 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.
@@ -2986,39 +2986,47 @@ impl App {
.categories
.values()
.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([
Cell::from(if self.video_state.is_watched(&video.id) {
"✓"
} else {
""
}),
Cell::from(Text::from(vec![
Line::from(
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
.video_state
.contains(CollectionKind::WatchLater, &video.id)
{
Cell::from(video.title.as_str()).fg(Color::Magenta)
title.fg(Color::Magenta)
} else {
Cell::from(video.title.as_str())
title
},
),
Line::from(vec![
if tagged {
channel.fg(Color::Cyan)
} else {
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(3),
Constraint::Fill(1),
Constraint::Length(11),
],
[Constraint::Length(1), Constraint::Fill(1)],
)
.header(Row::new(["", "Video", "Channel", "Published"]).bold())
.block(Block::bordered().title(loading_title(
if view.source == ViewSource::Search {
" 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))
.unwrap();
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() {
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 x = row.find(label).expect("channel label is visible") as u16;
assert_eq!(
@@ -3024,8 +3024,17 @@ fn video_channel_colors_follow_category_membership_and_preserve_selection_contra
},
"{label} in row {index}, cleared={cleared}"
);
assert_eq!(buffer[(3, y)].fg, Color::Reset, "title stays uncolored");
assert_eq!(buffer[(108, y)].fg, Color::Reset, "date stays uncolored");
let title: String = (0..120).map(|x| buffer[(x, y - 1)].symbol()).collect();
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 {
app.views[0].selection.select(Some(0));