use crate::widgets::*;
use crate::*;
use crossterm::event::MouseEvent;
use gonk_player::Player;
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use tui::style::{Color, Modifier, Style};
use tui::text::{Span, Spans};
use tui::widgets::{Block, BorderType, Borders, Paragraph};
use unicode_width::UnicodeWidthStr;
pub struct Queue {
pub ui: Index<()>,
pub constraint: [u16; 4],
pub len: usize,
}
impl Queue {
pub fn new() -> Self {
Self {
ui: Index::new(Vec::new(), Some(0)),
constraint: [6, 37, 31, 26],
len: 0,
}
}
}
impl Input for Queue {
fn up(&mut self) {
self.ui.up_with_len(self.len);
}
fn down(&mut self) {
self.ui.down_with_len(self.len);
}
fn left(&mut self) {}
fn right(&mut self) {}
}
pub fn constraint(queue: &mut Queue, row: usize, shift: bool) {
if shift && queue.constraint[row] != 0 {
queue.constraint[row + 1] += 1;
queue.constraint[row] = queue.constraint[row].saturating_sub(1);
} else if queue.constraint[row + 1] != 0 {
queue.constraint[row] += 1;
queue.constraint[row + 1] = queue.constraint[row + 1].saturating_sub(1);
}
debug_assert!(
queue.constraint.iter().sum::<u16>() == 100,
"Constraint went out of bounds: {:?}",
queue.constraint
);
}
pub fn delete(queue: &mut Queue, player: &mut Player) {
if let Some(i) = queue.ui.index() {
match player.delete_index(i) {
Ok(_) => save_queue(player),
Err(e) => log!("{}", e),
};
let len = player.songs.len().saturating_sub(1);
if i > len {
queue.ui.select(Some(len));
}
}
}
pub fn draw(queue: &mut Queue, player: &mut Player, f: &mut Frame, event: Option<MouseEvent>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(10),
Constraint::Length(3),
])
.split(f.size());
draw_header(player, f, chunks[0]);
let row_bounds = draw_body(queue, player, f, chunks[1]);
if log::message().is_none() {
draw_seeker(player, f, chunks[2]);
}
if player.songs.is_empty() {
return;
}
if let Some(event) = event {
let (x, y) = (event.column, event.row);
let header_height = 5;
let size = f.size();
if (size.height - 3 == y || size.height - 2 == y || size.height - 1 == y)
&& size.height > 15
{
let ratio = x as f32 / size.width as f32;
let duration = player.duration().as_secs_f32();
match player.seek_to(duration * ratio) {
Ok(_) => (),
Err(e) => log!("{}", e),
};
}
if let Some((start, _)) = row_bounds {
if y >= header_height {
let index = (y - header_height) as usize + start;
if index < player.songs.len()
&& ((size.height < 15 && y < size.height.saturating_sub(1))
|| y < size.height.saturating_sub(3))
{
queue.ui.select(Some(index));
}
}
}
}
}
fn draw_header(player: &mut Player, f: &mut Frame, area: Rect) {
f.render_widget(
Block::default()
.borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Rounded),
area,
);
let state = if player.songs.is_empty() {
String::from("â•â”€Stopped")
} else if player.is_playing() {
String::from("â•â”€Playing")
} else {
String::from("â•â”€Paused")
};
f.render_widget(Paragraph::new(state).alignment(Alignment::Left), area);
if !player.songs.is_empty() {
draw_title(player, f, area);
}
let volume = Spans::from(format!("Vol: {}%─╮", player.volume));
f.render_widget(Paragraph::new(volume).alignment(Alignment::Right), area);
}
fn draw_title(player: &mut Player, f: &mut Frame, area: Rect) {
let title = if let Some(song) = player.songs.selected() {
let mut artist = song.artist.trim_end().to_string();
let mut album = song.album.trim_end().to_string();
let mut title = song.title.trim_end().to_string();
let max_width = area.width.saturating_sub(30) as usize;
while artist.width() + album.width() + "-| - |-".width() > max_width {
if artist.width() > album.width() {
artist.pop();
} else {
album.pop();
}
}
while title.width() > max_width {
title.pop();
}
let n = title
.width()
.saturating_sub(artist.width() + album.width() + 3);
let rem = n % 2;
let pad_front = " ".repeat(n / 2);
let pad_back = " ".repeat(n / 2 + rem);
vec![
Spans::from(vec![
Span::raw(format!("─│ {}", pad_front)),
Span::styled(artist, Style::default().fg(COLORS.artist)),
Span::raw(" ─ "),
Span::styled(album, Style::default().fg(COLORS.album)),
Span::raw(format!("{} │─", pad_back)),
]),
Spans::from(Span::styled(title, Style::default().fg(COLORS.title))),
]
} else {
Vec::new()
};
f.render_widget(Paragraph::new(title).alignment(Alignment::Center), area);
}
fn draw_body(
queue: &mut Queue,
player: &mut Player,
f: &mut Frame,
area: Rect,
) -> Option<(usize, usize)> {
if log::message().is_some() && player.songs.is_empty() {
f.render_widget(
Block::default()
.border_type(BorderType::Rounded)
.borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM),
area,
);
return None;
}
if player.songs.is_empty() {
f.render_widget(
Block::default()
.border_type(BorderType::Rounded)
.borders(Borders::LEFT | Borders::RIGHT),
area,
);
return None;
}
let (songs, player_index, ui_index) =
(&player.songs.data, player.songs.index(), queue.ui.index());
let mut items: Vec<Row> = songs
.iter()
.map(|song| {
Row::new(vec![
Cell::from(""),
Cell::from(song.number.to_string()).style(Style::default().fg(COLORS.number)),
Cell::from(song.title.as_str()).style(Style::default().fg(COLORS.title)),
Cell::from(song.album.as_str()).style(Style::default().fg(COLORS.album)),
Cell::from(song.artist.as_str()).style(Style::default().fg(COLORS.artist)),
])
})
.collect();
if let Some(player_index) = player_index {
if let Some(song) = songs.get(player_index) {
if let Some(ui_index) = ui_index {
let row = if ui_index == player_index {
Row::new(vec![
Cell::from(">>").style(
Style::default()
.fg(Color::White)
.add_modifier(Modifier::DIM | Modifier::BOLD),
),
Cell::from(song.number.to_string())
.style(Style::default().bg(COLORS.number).fg(Color::Black)),
Cell::from(song.title.as_str())
.style(Style::default().bg(COLORS.title).fg(Color::Black)),
Cell::from(song.album.as_str())
.style(Style::default().bg(COLORS.album).fg(Color::Black)),
Cell::from(song.artist.as_str())
.style(Style::default().bg(COLORS.artist).fg(Color::Black)),
])
} else {
Row::new(vec![
Cell::from(">>").style(
Style::default()
.fg(Color::White)
.add_modifier(Modifier::DIM | Modifier::BOLD),
),
Cell::from(song.number.to_string())
.style(Style::default().fg(COLORS.number)),
Cell::from(song.title.as_str()).style(Style::default().fg(COLORS.title)),
Cell::from(song.album.as_str()).style(Style::default().fg(COLORS.album)),
Cell::from(song.artist.as_str()).style(Style::default().fg(COLORS.artist)),
])
};
items.remove(player_index);
items.insert(player_index, row);
if ui_index != player_index {
if let Some(song) = songs.get(ui_index) {
let row = Row::new(vec![
Cell::default(),
Cell::from(song.number.to_string())
.style(Style::default().bg(COLORS.number)),
Cell::from(song.title.as_str())
.style(Style::default().bg(COLORS.title)),
Cell::from(song.album.as_str())
.style(Style::default().bg(COLORS.album)),
Cell::from(song.artist.as_str())
.style(Style::default().bg(COLORS.artist)),
])
.style(Style::default().fg(Color::Black));
items.remove(ui_index);
items.insert(ui_index, row);
}
}
}
}
}
let con = [
Constraint::Length(2),
Constraint::Percentage(queue.constraint[0]),
Constraint::Percentage(queue.constraint[1]),
Constraint::Percentage(queue.constraint[2]),
Constraint::Percentage(queue.constraint[3]),
];
let t = Table::new(&items)
.header(
Row::new(["", "#", "Title", "Album", "Artist"])
.style(
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)
.bottom_margin(1),
)
.block(
Block::default()
.borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)
.border_type(BorderType::Rounded),
)
.widths(&con);
let row_bounds = t.get_row_bounds(ui_index, t.get_row_height(area));
f.render_stateful_widget(t, area, &mut TableState::new(ui_index));
Some(row_bounds)
}
fn draw_seeker(player: &mut Player, f: &mut Frame, area: Rect) {
if player.songs.is_empty() {
return f.render_widget(
Block::default()
.border_type(BorderType::Rounded)
.borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT),
area,
);
}
let elapsed = player.elapsed().as_secs_f64();
let duration = player.duration().as_secs_f64();
let seeker = format!(
"{:02}:{:02}/{:02}:{:02}",
(elapsed / 60.0).floor(),
(elapsed % 60.0) as u64,
(duration / 60.0).floor(),
(duration % 60.0) as u64,
);
let ratio = elapsed.floor() / duration;
let ratio = if ratio.is_nan() {
0.0
} else {
ratio.clamp(0.0, 1.0)
};
f.render_widget(
Gauge::default()
.block(
Block::default()
.border_type(BorderType::Rounded)
.borders(Borders::ALL),
)
.gauge_style(Style::default().fg(COLORS.seeker))
.ratio(ratio as f64)
.label(seeker),
area,
);
}