feat: show YouTube videos as two-line entries
This commit is contained in:
@@ -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
@@ -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
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user