mirror of
https://github.com/socatlolmeow/drift.git
synced 2026-11-03 19:27:19 +00:00
(add): add confirmation for deleting playlists + fix playlist name overwriting
This commit is contained in:
parent
bcff53c373
commit
f1eb55801f
8 changed files with 161 additions and 353 deletions
162
src/ui.rs
162
src/ui.rs
|
|
@ -11,10 +11,7 @@ const ACCENT: Color = Color::Cyan;
|
|||
const SPINNER: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
|
||||
fn selected_style() -> Style {
|
||||
Style::new()
|
||||
.fg(Color::Black)
|
||||
.bg(ACCENT)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
Style::new().fg(Color::Black).bg(ACCENT).add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
fn header_style() -> Style {
|
||||
|
|
@ -59,10 +56,7 @@ fn draw_tabs(f: &mut Frame, app: &App, area: Rect) {
|
|||
])
|
||||
.areas(area);
|
||||
|
||||
f.render_widget(
|
||||
Paragraph::new(" ♪ Drift").style(Style::new().fg(ACCENT).bold()),
|
||||
name,
|
||||
);
|
||||
f.render_widget(Paragraph::new(" ♪ Drift").style(Style::new().fg(ACCENT).bold()), name);
|
||||
|
||||
let titles = [
|
||||
format!(" Results {} ", app.results.len()),
|
||||
|
|
@ -113,11 +107,7 @@ fn track_rows<'a>(
|
|||
.enumerate()
|
||||
.map(|(i, t)| {
|
||||
let is_playing = playing.is_some_and(|(id, _)| id == t.id);
|
||||
let marker = if is_playing {
|
||||
playing.map_or("", |p| p.1)
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let marker = if is_playing { playing.map_or("", |p| p.1) } else { "" };
|
||||
let mut cells = vec![
|
||||
Cell::from(marker),
|
||||
Cell::from(format!("{:>3}", i + 1)).style(dim()),
|
||||
|
|
@ -128,7 +118,8 @@ fn track_rows<'a>(
|
|||
cells.push(Cell::from(t.album.as_deref().unwrap_or("")));
|
||||
}
|
||||
cells.push(
|
||||
Cell::from(t.duration.map_or(String::new(), |d| fmt_time(d as f64))).style(dim()),
|
||||
Cell::from(t.duration.map_or(String::new(), |d| fmt_time(d as f64)))
|
||||
.style(dim()),
|
||||
);
|
||||
let row = Row::new(cells);
|
||||
if is_playing {
|
||||
|
|
@ -165,9 +156,7 @@ fn track_table<'a>(rows: Vec<Row<'a>>, show_album: bool, block: Block<'a>) -> Ta
|
|||
fn empty_hint(f: &mut Frame, block: Block, area: Rect, lines: &[&str]) {
|
||||
let text: Vec<Line> = lines.iter().map(|l| Line::from(*l).style(dim())).collect();
|
||||
f.render_widget(
|
||||
Paragraph::new(text)
|
||||
.alignment(Alignment::Center)
|
||||
.block(block),
|
||||
Paragraph::new(text).alignment(Alignment::Center).block(block),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
|
@ -187,17 +176,9 @@ fn draw_tracks(f: &mut Frame, app: &mut App, area: Rect, view: View) {
|
|||
|
||||
if tracks.is_empty() {
|
||||
let hint: &[&str] = if view == View::Queue {
|
||||
&[
|
||||
"",
|
||||
"The queue is empty.",
|
||||
"Press `a` on a search result to queue it.",
|
||||
]
|
||||
&["", "The queue is empty.", "Press `a` on a search result to queue it."]
|
||||
} else {
|
||||
&[
|
||||
"",
|
||||
"No results yet.",
|
||||
"Press / and type a query, or use :search \"daft punk\"",
|
||||
]
|
||||
&["", "No results yet.", "Press / and type a query, or use :search \"daft punk\""]
|
||||
};
|
||||
return empty_hint(f, block, area, hint);
|
||||
}
|
||||
|
|
@ -211,9 +192,7 @@ fn draw_open_playlist(f: &mut Frame, app: &mut App, area: Rect, name: &str) {
|
|||
let now = playing_marker(app);
|
||||
let playing = now.as_ref().map(|(id, marker)| (id.as_str(), *marker));
|
||||
|
||||
let Some(tracks) = app.store.playlists.get(name) else {
|
||||
return;
|
||||
};
|
||||
let Some(tracks) = app.store.playlists.get(name) else { return };
|
||||
let block = Block::bordered()
|
||||
.title(format!(" {name} ({}) ", tracks.len()))
|
||||
.border_style(Style::new().fg(ACCENT));
|
||||
|
|
@ -222,11 +201,7 @@ fn draw_open_playlist(f: &mut Frame, app: &mut App, area: Rect, name: &str) {
|
|||
f,
|
||||
block,
|
||||
area,
|
||||
&[
|
||||
"",
|
||||
"This playlist is empty.",
|
||||
"Add songs with :addto \"name\" from Results or Queue.",
|
||||
],
|
||||
&["", "This playlist is empty.", "Add songs with :addto \"name\" from Results or Queue."],
|
||||
);
|
||||
}
|
||||
let show_album = area.width >= 76;
|
||||
|
|
@ -253,12 +228,7 @@ fn draw_playlists(f: &mut Frame, app: &mut App, area: Rect) {
|
|||
f,
|
||||
block,
|
||||
left,
|
||||
&[
|
||||
"",
|
||||
"No playlists yet.",
|
||||
"Queue some songs, then",
|
||||
":save \"name\"",
|
||||
],
|
||||
&["", "No playlists yet.", "Queue some songs, then", ":save \"name\""],
|
||||
);
|
||||
let b = Block::bordered().title(" Preview ").border_style(dim());
|
||||
f.render_widget(b, right);
|
||||
|
|
@ -280,11 +250,7 @@ fn draw_playlists(f: &mut Frame, app: &mut App, area: Rect) {
|
|||
.collect();
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(5),
|
||||
],
|
||||
[Constraint::Length(3), Constraint::Fill(1), Constraint::Length(5)],
|
||||
)
|
||||
.header(Row::new(vec!["", "Name", "Songs"]).style(header_style()))
|
||||
.block(block)
|
||||
|
|
@ -293,10 +259,7 @@ fn draw_playlists(f: &mut Frame, app: &mut App, area: Rect) {
|
|||
|
||||
let sel = app.selected();
|
||||
let preview_block = Block::bordered().title(" Preview ").border_style(dim());
|
||||
match app
|
||||
.playlist_name(sel)
|
||||
.and_then(|n| app.store.playlists.get(&n))
|
||||
{
|
||||
match app.playlist_name(sel).and_then(|n| app.store.playlists.get(&n)) {
|
||||
Some(tracks) if !tracks.is_empty() => {
|
||||
let show_album = right.width >= 70;
|
||||
let rows = track_rows(tracks, None, show_album);
|
||||
|
|
@ -325,12 +288,7 @@ fn draw_now_playing(f: &mut Frame, app: &App, area: Rect) {
|
|||
let flags_w = flags.chars().count() as u16 + 1;
|
||||
let [title_area, flags_area] =
|
||||
Layout::horizontal([Constraint::Min(1), Constraint::Length(flags_w)]).areas(l1);
|
||||
f.render_widget(
|
||||
Paragraph::new(flags)
|
||||
.style(dim())
|
||||
.alignment(Alignment::Right),
|
||||
flags_area,
|
||||
);
|
||||
f.render_widget(Paragraph::new(flags).style(dim()).alignment(Alignment::Right), flags_area);
|
||||
|
||||
let icon = match app.play_state {
|
||||
PlayState::Playing => "▶".to_string(),
|
||||
|
|
@ -356,14 +314,7 @@ fn draw_now_playing(f: &mut Frame, app: &App, area: Rect) {
|
|||
f.render_widget(Paragraph::new(line), title_area);
|
||||
|
||||
let left = format!(" {} ", fmt_time(app.position));
|
||||
let right = format!(
|
||||
" {} ",
|
||||
if app.duration > 0.0 {
|
||||
fmt_time(app.duration)
|
||||
} else {
|
||||
"--:--".into()
|
||||
}
|
||||
);
|
||||
let right = format!(" {} ", if app.duration > 0.0 { fmt_time(app.duration) } else { "--:--".into() });
|
||||
let bar_w = (l2.width as usize).saturating_sub(left.chars().count() + right.chars().count());
|
||||
let ratio = if app.duration > 0.0 {
|
||||
(app.position / app.duration).clamp(0.0, 1.0)
|
||||
|
|
@ -381,12 +332,19 @@ fn draw_now_playing(f: &mut Frame, app: &App, area: Rect) {
|
|||
}
|
||||
|
||||
fn draw_cmdline(f: &mut Frame, app: &App, area: Rect) {
|
||||
if app.mode == Mode::ConfirmDelete {
|
||||
let name = app.pending_delete.as_deref().unwrap_or_default();
|
||||
f.render_widget(
|
||||
Paragraph::new(format!(" Delete playlist \u{201c}{name}\u{201d}? (y/N)"))
|
||||
.style(Style::new().fg(Color::Yellow).bold()),
|
||||
area,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if app.mode == Mode::Command {
|
||||
let text = format!(":{}", app.cmd_buf);
|
||||
let col = text
|
||||
.chars()
|
||||
.count()
|
||||
.min(area.width.saturating_sub(1) as usize) as u16;
|
||||
let col = text.chars().count().min(area.width.saturating_sub(1) as usize) as u16;
|
||||
f.render_widget(Paragraph::new(text), area);
|
||||
f.set_cursor_position((area.x + col, area.y));
|
||||
return;
|
||||
|
|
@ -413,9 +371,7 @@ fn draw_cmdline(f: &mut Frame, app: &App, area: Rect) {
|
|||
}
|
||||
if !pending.is_empty() {
|
||||
f.render_widget(
|
||||
Paragraph::new(format!("{pending} "))
|
||||
.alignment(Alignment::Right)
|
||||
.style(Style::new().fg(Color::Yellow)),
|
||||
Paragraph::new(format!("{pending} ")).alignment(Alignment::Right).style(Style::new().fg(Color::Yellow)),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
|
@ -432,25 +388,16 @@ const HELP: &[(&str, &str)] = &[
|
|||
("Tab S-Tab", "cycle Results / Queue / Playlists"),
|
||||
("gr gq gp", "jump to Results / Queue / Playlists"),
|
||||
("PLAYBACK", "#"),
|
||||
(
|
||||
"Enter",
|
||||
"play selected (on a playlist: load it; inside one: play from there)",
|
||||
),
|
||||
("Enter", "play selected (on a playlist: load it; inside one: play from there)"),
|
||||
("Space", "pause / resume"),
|
||||
("n N (or > <)", "next / previous track"),
|
||||
("h l / ← →", "seek -5s / +5s (counts: 6l)"),
|
||||
("+ -", "volume up / down"),
|
||||
("s r", "toggle shuffle / cycle repeat"),
|
||||
(
|
||||
"ga or :artist",
|
||||
"songs by the artist of the selected song; a popup asks which one when there are several",
|
||||
),
|
||||
("ga or :artist", "songs by the artist of the selected song; a popup asks which one when there are several"),
|
||||
("QUEUE", "#"),
|
||||
("a A P", "add selected / add all / play all results"),
|
||||
(
|
||||
"dd x",
|
||||
"remove from queue / delete playlist / delete track in a playlist",
|
||||
),
|
||||
("dd x", "remove from queue / delete playlist / delete track in a playlist"),
|
||||
("J K", "move queue or playlist item down / up"),
|
||||
("PLAYLISTS", "#"),
|
||||
("→ or l", "open the highlighted playlist"),
|
||||
|
|
@ -464,19 +411,10 @@ const HELP: &[(&str, &str)] = &[
|
|||
(":vol 50 | +5", "set or change the volume"),
|
||||
(":shuffle :repeat [m]", "modes: off, all or one"),
|
||||
(":add [N] :addall", "queue a result / all results"),
|
||||
(
|
||||
":playall :remove [N]",
|
||||
"play all results / remove from queue",
|
||||
),
|
||||
(":playall :remove [N]", "play all results / remove from queue"),
|
||||
(":clear", "empty the queue"),
|
||||
(
|
||||
":save n :load n",
|
||||
"save the queue as a playlist / load one",
|
||||
),
|
||||
(
|
||||
":append n :delpl n",
|
||||
"append a playlist to the queue / delete it",
|
||||
),
|
||||
(":save n :load n", "save the queue as a playlist / load one"),
|
||||
(":append n :delpl n", "append a playlist to the queue / delete it"),
|
||||
(":addto n", "add the selected track to playlist n"),
|
||||
(":import URL", "load a YouTube / YT Music playlist"),
|
||||
(":artist", "same as ga"),
|
||||
|
|
@ -532,10 +470,7 @@ fn help_lines(inner_w: usize) -> Vec<Line<'static>> {
|
|||
}
|
||||
}
|
||||
for rest in desc {
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" ".repeat(key_w)),
|
||||
Span::raw(rest),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::raw(" ".repeat(key_w)), Span::raw(rest)]));
|
||||
}
|
||||
}
|
||||
lines
|
||||
|
|
@ -543,12 +478,7 @@ fn help_lines(inner_w: usize) -> Vec<Line<'static>> {
|
|||
|
||||
fn draw_artist_picker(f: &mut Frame, app: &App, area: Rect) {
|
||||
let n = app.artist_opts.len() as u16;
|
||||
let widest = app
|
||||
.artist_opts
|
||||
.iter()
|
||||
.map(|a| a.chars().count())
|
||||
.max()
|
||||
.unwrap_or(0) as u16;
|
||||
let widest = app.artist_opts.iter().map(|a| a.chars().count()).max().unwrap_or(0) as u16;
|
||||
let w = (widest + 10).max(34).min(area.width);
|
||||
let h = (n + 3).min(area.height);
|
||||
let popup = Rect {
|
||||
|
|
@ -603,12 +533,7 @@ fn draw_help(f: &mut Frame, app: &mut App, area: Rect) {
|
|||
.border_style(Style::new().fg(Color::Yellow));
|
||||
if max_scroll > 0 {
|
||||
let end = (scroll + visible).min(lines.len());
|
||||
block = block.title_bottom(format!(
|
||||
" j/k scroll · {}-{} of {} ",
|
||||
scroll + 1,
|
||||
end,
|
||||
lines.len()
|
||||
));
|
||||
block = block.title_bottom(format!(" j/k scroll · {}-{} of {} ", scroll + 1, end, lines.len()));
|
||||
}
|
||||
|
||||
let shown: Vec<Line> = lines.into_iter().skip(scroll).take(visible).collect();
|
||||
|
|
@ -626,12 +551,7 @@ mod tests {
|
|||
use std::sync::mpsc;
|
||||
|
||||
fn text(term: &Terminal<TestBackend>) -> String {
|
||||
term.backend()
|
||||
.buffer()
|
||||
.content()
|
||||
.iter()
|
||||
.map(|c| c.symbol())
|
||||
.collect()
|
||||
term.backend().buffer().content().iter().map(|c| c.symbol()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -651,13 +571,7 @@ mod tests {
|
|||
duration: Some(90),
|
||||
};
|
||||
app.store.playlists.insert("empty".into(), vec![]);
|
||||
app.store.playlists.insert(
|
||||
"mix".into(),
|
||||
vec![
|
||||
t("aaaaaaaaaaa", "First Song"),
|
||||
t("bbbbbbbbbbb", "Second Song"),
|
||||
],
|
||||
);
|
||||
app.store.playlists.insert("mix".into(), vec![t("aaaaaaaaaaa", "First Song"), t("bbbbbbbbbbb", "Second Song")]);
|
||||
let key = |app: &mut App, c| app.on_key(KeyEvent::new(c, KeyModifiers::NONE));
|
||||
|
||||
let mut term = Terminal::new(TestBackend::new(100, 24)).unwrap();
|
||||
|
|
|
|||
Loading…
Reference in a new issue