use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Margin, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui_image::Image;
use unicode_width::UnicodeWidthStr;
use super::{border, bordered, fit, hint_spans, video_info, wrap};
use crate::app::{App, InfoTab, WatchPane};
use crate::images::{Key, Subject};
use crate::pepper;
use crate::theme::Colors;
use crate::video::{self, Video};
const UP_NEXT_BESIDE: u16 = 100;
const ROWS_UNDER: u16 = 8;
const UNDER_PART: u16 = 3;
pub fn draw(frame: &mut Frame, app: &mut App, area: Rect) {
let Some(watch) = app.watch.as_ref() else {
return;
};
let c = app.colors.clone();
let pane = watch.pane;
let video = watch.info.video.clone();
let beside = area.width >= UP_NEXT_BESIDE;
let side_width = if beside {
(area.width / 3).clamp(32, 60)
} else {
0
};
let [main, side] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(side_width)]).areas(area);
let hint = match (pane, watch.info.tab) {
(WatchPane::Info, InfoTab::Description) => {
" `Tab` comments · `Ctrl-l` up next · `Esc` back "
}
(WatchPane::Info, InfoTab::Comments) => {
" `Tab` description · `Ctrl-l` up next · `Esc` back "
}
(WatchPane::UpNext, InfoTab::Description) => {
" `Enter` play · `Ctrl-h` description · `Esc` back "
}
(WatchPane::UpNext, InfoTab::Comments) => " `Enter` play · `Ctrl-h` comments · `Esc` back ",
};
let block = bordered()
.border_style(border(pane == WatchPane::Info || !beside, &c))
.title(Line::from(vec![
Span::raw(" "),
Span::styled(app.icons.play, Style::new().fg(c.accent)),
Span::raw(" Watch "),
]))
.title_bottom(Line::from(hint_spans(hint, &c)).right_aligned());
let inner = block.inner(main).inner(Margin::new(1, 0));
frame.render_widget(block, main);
let font = app.images.font_size();
let under = ROWS_UNDER.max(inner.height / UNDER_PART);
let max_rows = inner.height.saturating_sub(under).max(3);
let size = pepper::video_area(inner, (font.width, font.height), max_rows);
let picture = Rect {
x: inner.x + (inner.width - size.width) / 2,
..size
};
draw_picture(frame, app, &c, &video, picture);
let under = Rect {
y: picture.bottom() + 1,
height: inner.bottom().saturating_sub(picture.bottom() + 1),
..inner
};
if under.height > 0 {
if beside || pane == WatchPane::Info {
info(frame, app, &c, under, pane == WatchPane::Info);
} else {
up_next(frame, app, &c, under, true);
}
}
if beside {
let block = bordered()
.border_style(border(pane == WatchPane::UpNext, &c))
.title(" Up next ");
let inner = block.inner(side).inner(Margin::new(1, 0));
frame.render_widget(block, side);
up_next(frame, app, &c, inner, pane == WatchPane::UpNext);
}
}
fn draw_picture(frame: &mut Frame, app: &mut App, c: &Colors, video: &Video, area: Rect) {
if area.is_empty() {
return;
}
let inline = app
.playing
.as_ref()
.is_some_and(|p| p.video.id == video.id && p.pepper.is_some());
if inline {
frame.render_widget(Block::new().style(Style::new().bg(Color::Black)), area);
app.video_box.set(Some(area));
return;
}
let covered = app.popup_open() && app.images.paints_over();
let key = Key {
subject: Subject::Thumbnail(video.id.clone()),
cols: area.width,
rows: area.height,
};
let drawn = !covered
&& match app.images.get(&key, &video.thumbnail_urls()) {
Some(image) => {
frame.render_widget(Image::new(image), area);
true
}
None => false,
};
if drawn {
app.images.place(&key.subject, area, area);
return;
}
frame.render_widget(Block::new().style(Style::new().bg(c.selected)), area);
let mark = if app.images.is_broken(&key) {
"✕"
} else {
app.icons.play
};
let middle = Rect {
x: area.x + area.width / 2,
y: area.y + area.height / 2,
width: 1,
height: 1,
};
frame.render_widget(
Paragraph::new(Span::styled(mark, Style::new().fg(c.dim))),
middle,
);
}
fn info(frame: &mut Frame, app: &mut App, c: &Colors, area: Rect, active: bool) {
let title = app
.watch
.as_ref()
.map(|w| app.title(&w.info.video).to_string())
.unwrap_or_default();
let Some(watch) = app.watch.as_mut() else {
return;
};
let tab = |label: &'static str, on: bool| {
if on && active {
Span::styled(
label,
Style::new()
.fg(c.bg)
.bg(c.accent)
.add_modifier(Modifier::BOLD),
)
} else if on {
Span::styled(label, Style::new().fg(c.text).add_modifier(Modifier::BOLD))
} else {
Span::styled(label, Style::new().fg(c.dim))
}
};
let tabs = Line::from(vec![
tab(" Description ", watch.info.tab == InfoTab::Description),
Span::raw(" "),
tab(" Comments ", watch.info.tab == InfoTab::Comments),
]);
video_info(
frame,
c,
&app.comments,
&mut watch.info,
&title,
area,
Some(tabs),
active,
);
}
fn up_next(frame: &mut Frame, app: &mut App, c: &Colors, area: Rect, active: bool) {
let list: Vec<(Video, bool)> = app
.up_next_list()
.into_iter()
.map(|(video, from_jukebox)| (video.clone(), from_jukebox))
.collect();
if list.is_empty() {
let lines = vec![
Line::styled("Nothing up next.", Style::new().fg(c.subtext)),
Line::default(),
]
.into_iter()
.chain(
wrap(
"A video played from a list plays the rest of it after (with autoplay, A); e puts one in the Jukebox, which plays first.",
usize::from(area.width),
)
.into_iter()
.map(|row| Line::styled(row, Style::new().fg(c.dim))),
)
.collect::<Vec<_>>();
frame.render_widget(Paragraph::new(lines), area);
return;
}
let photos = app.images.draws_photos();
let font = app.images.font_size();
let thumb = if photos {
let cols = (area.width / 3).clamp(10, 22);
pepper::video_area(
Rect::new(0, 0, cols, area.height),
(font.width, font.height),
6,
)
} else {
Rect::default()
};
let rows_each = thumb.height.max(3) + 1;
let shown = usize::from((area.height / rows_each).max(1));
let Some(watch) = app.watch.as_ref() else {
return;
};
let selected = watch.next_selected.min(list.len() - 1);
let mut scroll = watch.next_scroll.get().min(list.len() - 1);
if selected < scroll {
scroll = selected;
} else if selected >= scroll + shown {
scroll = selected + 1 - shown;
}
watch.next_scroll.set(scroll);
for (row, (video, from_jukebox)) in list.iter().enumerate().skip(scroll).take(shown) {
let y = area.y + (row - scroll) as u16 * rows_each;
let item = Rect {
y,
height: rows_each
.saturating_sub(1)
.min(area.bottom().saturating_sub(y)),
..area
};
if item.is_empty() {
break;
}
let picked = active && row == selected;
if picked {
frame.render_widget(Block::new().style(Style::new().bg(c.selected)), item);
}
let text_x = if photos {
let place = Rect {
x: item.x,
y: item.y,
width: thumb.width.min(item.width),
height: thumb.height.min(item.height),
};
let key = Key {
subject: Subject::Thumbnail(video.id.clone()),
cols: place.width,
rows: place.height,
};
let covered = app.popup_open() && app.images.paints_over();
match app.images.get(&key, &video.thumbnail_urls()) {
Some(image) if !covered && place.height == thumb.height => {
frame.render_widget(Image::new(image), place);
app.images.place(&key.subject, place, place);
}
_ => {
frame.render_widget(Block::new().style(Style::new().bg(c.panel)), place);
}
}
item.x + place.width + 1
} else {
item.x
};
let text = Rect {
x: text_x,
width: item.right().saturating_sub(text_x),
..item
};
let columns = usize::from(text.width);
let mut lines: Vec<Line> = wrap(app.title(video), columns)
.into_iter()
.take(2)
.map(|row| Line::styled(row, Style::new().fg(c.text).add_modifier(Modifier::BOLD)))
.collect();
let mut about = vec![video.channel.clone()];
about.extend(video.duration.map(video::duration));
about.retain(|s| !s.is_empty());
let about = about.join(" · ");
let mark = if *from_jukebox { " · Jukebox" } else { "" };
lines.push(Line::from(vec![
Span::styled(
fit(&about, columns.saturating_sub(mark.width())),
Style::new().fg(c.dim),
),
Span::styled(mark, Style::new().fg(c.accent)),
]));
frame.render_widget(Paragraph::new(lines), text);
}
}