mirror of
https://github.com/socatlolmeow/drift.git
synced 2026-11-03 19:27:19 +00:00
Merge pull request #2 from olivr2346:pr/save-album-as-playlist
feat: add native support for saving albums and results as playlists
This commit is contained in:
commit
43289e1c42
4 changed files with 231 additions and 2 deletions
|
|
@ -134,6 +134,7 @@ Press `/` to search, `Enter` to play, `a` to queue and `?` for the full list of
|
||||||
| `:shuffle` `:repeat [off\|all\|one]` | playback modes |
|
| `:shuffle` `:repeat [off\|all\|one]` | playback modes |
|
||||||
| `:add [N]` `:addall` `:playall` | queue results |
|
| `:add [N]` `:addall` `:playall` | queue results |
|
||||||
| `:save name` `:load name` `:append name` | work with playlists |
|
| `:save name` `:load name` `:append name` | work with playlists |
|
||||||
|
| `:savealbum [name]` `:saveresults [name]` | save album or results directly as a playlist |
|
||||||
| `:addto name` `:delpl name` | add a song to / delete a playlist |
|
| `:addto name` `:delpl name` | add a song to / delete a playlist |
|
||||||
| `:import URL` | import a YouTube / YouTube Music playlist |
|
| `:import URL` | import a YouTube / YouTube Music playlist |
|
||||||
| `:album [query]` | open the selected song's album, or search albums |
|
| `:album [query]` | open the selected song's album, or search albums |
|
||||||
|
|
|
||||||
198
src/app.rs
198
src/app.rs
|
|
@ -9,7 +9,7 @@ use ratatui::widgets::TableState;
|
||||||
use crate::command::{self, Amount, Command, COMMAND_NAMES};
|
use crate::command::{self, Amount, Command, COMMAND_NAMES};
|
||||||
use crate::config::{self, Config};
|
use crate::config::{self, Config};
|
||||||
use crate::lyrics::{self, Lyrics};
|
use crate::lyrics::{self, Lyrics};
|
||||||
use crate::model::{Collection, RepeatMode, Track};
|
use crate::model::{split_artists, Collection, RepeatMode, Track};
|
||||||
use crate::discord::Discord;
|
use crate::discord::Discord;
|
||||||
use crate::mpris::{Cmd, Mpris};
|
use crate::mpris::{Cmd, Mpris};
|
||||||
use crate::player::{self, Player, PlayerEvent};
|
use crate::player::{self, Player, PlayerEvent};
|
||||||
|
|
@ -37,6 +37,10 @@ pub enum AppEvent {
|
||||||
title: String,
|
title: String,
|
||||||
result: Result<Vec<Collection>, String>,
|
result: Result<Vec<Collection>, String>,
|
||||||
},
|
},
|
||||||
|
SavePlaylist {
|
||||||
|
name: String,
|
||||||
|
result: Result<Vec<Track>, String>,
|
||||||
|
},
|
||||||
Media(Cmd),
|
Media(Cmd),
|
||||||
Audio {
|
Audio {
|
||||||
id: String,
|
id: String,
|
||||||
|
|
@ -114,6 +118,9 @@ pub enum MenuAction {
|
||||||
Artist,
|
Artist,
|
||||||
Similar,
|
Similar,
|
||||||
Album,
|
Album,
|
||||||
|
SaveTrackAlbum,
|
||||||
|
SaveAlbum,
|
||||||
|
SaveResults,
|
||||||
PlaylistMenu,
|
PlaylistMenu,
|
||||||
PickPlaylist(String),
|
PickPlaylist(String),
|
||||||
NewPlaylist,
|
NewPlaylist,
|
||||||
|
|
@ -543,6 +550,7 @@ impl App {
|
||||||
('g', 'a') => self.go_to_artist(),
|
('g', 'a') => self.go_to_artist(),
|
||||||
('g', 's') => self.go_similar(),
|
('g', 's') => self.go_similar(),
|
||||||
('g', 'b') => self.go_album(),
|
('g', 'b') => self.go_album(),
|
||||||
|
('g', 'B') => self.save_album(None),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -688,6 +696,7 @@ impl App {
|
||||||
items.push(menu_item('s', "Similar songs", MenuAction::Similar));
|
items.push(menu_item('s', "Similar songs", MenuAction::Similar));
|
||||||
if track.album.is_some() {
|
if track.album.is_some() {
|
||||||
items.push(menu_item('b', "Show album", MenuAction::Album));
|
items.push(menu_item('b', "Show album", MenuAction::Album));
|
||||||
|
items.push(menu_item('B', "Save album to playlist", MenuAction::SaveTrackAlbum));
|
||||||
}
|
}
|
||||||
items.push(menu_item('l', "Add to playlist…", MenuAction::PlaylistMenu));
|
items.push(menu_item('l', "Add to playlist…", MenuAction::PlaylistMenu));
|
||||||
if in_queue {
|
if in_queue {
|
||||||
|
|
@ -699,6 +708,11 @@ impl App {
|
||||||
if self.view == View::Results {
|
if self.view == View::Results {
|
||||||
items.push(menu_item('Q', "Queue all results", MenuAction::QueueAll));
|
items.push(menu_item('Q', "Queue all results", MenuAction::QueueAll));
|
||||||
items.push(menu_item('P', "Play all results", MenuAction::PlayAll));
|
items.push(menu_item('P', "Play all results", MenuAction::PlayAll));
|
||||||
|
if self.results_title.starts_with("Album: ") {
|
||||||
|
items.push(menu_item('S', "Save album as playlist…", MenuAction::SaveAlbum));
|
||||||
|
} else {
|
||||||
|
items.push(menu_item('S', "Save results as playlist…", MenuAction::SaveResults));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if in_queue {
|
if in_queue {
|
||||||
items.push(menu_item('c', "Clear queue", MenuAction::ClearQueue));
|
items.push(menu_item('c', "Clear queue", MenuAction::ClearQueue));
|
||||||
|
|
@ -806,6 +820,21 @@ impl App {
|
||||||
MenuAction::Artist => self.go_to_artist(),
|
MenuAction::Artist => self.go_to_artist(),
|
||||||
MenuAction::Similar => self.go_similar(),
|
MenuAction::Similar => self.go_similar(),
|
||||||
MenuAction::Album => self.go_album(),
|
MenuAction::Album => self.go_album(),
|
||||||
|
MenuAction::SaveTrackAlbum => {
|
||||||
|
if let Some(t) = track {
|
||||||
|
let album_title = t.album.clone().unwrap_or_else(|| "Album".to_string());
|
||||||
|
self.save_album_background(t, album_title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MenuAction::SaveAlbum => {
|
||||||
|
let default_name = self.results_title.strip_prefix("Album: ").unwrap_or("").trim();
|
||||||
|
if !default_name.is_empty() {
|
||||||
|
self.prefill_command(&format!("savealbum \"{default_name}\""));
|
||||||
|
} else {
|
||||||
|
self.prefill_command("savealbum ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MenuAction::SaveResults => self.prefill_command("saveresults "),
|
||||||
MenuAction::PlaylistMenu => {}
|
MenuAction::PlaylistMenu => {}
|
||||||
MenuAction::PickPlaylist(name) => self.add_to_playlist(&name),
|
MenuAction::PickPlaylist(name) => self.add_to_playlist(&name),
|
||||||
MenuAction::NewPlaylist => self.prefill_command("addto "),
|
MenuAction::NewPlaylist => self.prefill_command("addto "),
|
||||||
|
|
@ -1134,6 +1163,8 @@ impl App {
|
||||||
}
|
}
|
||||||
Command::Save(name) => self.save_playlist(&name, false),
|
Command::Save(name) => self.save_playlist(&name, false),
|
||||||
Command::SaveForce(name) => self.save_playlist(&name, true),
|
Command::SaveForce(name) => self.save_playlist(&name, true),
|
||||||
|
Command::SaveAlbum(name) => self.save_album(name),
|
||||||
|
Command::SaveResults(name) => self.save_results_as_playlist(name),
|
||||||
Command::Load(name) => match self.store.resolve(&name) {
|
Command::Load(name) => match self.store.resolve(&name) {
|
||||||
Some(n) => self.load_playlist(&n, true),
|
Some(n) => self.load_playlist(&n, true),
|
||||||
None => self.error(format!("no playlist named '{name}'")),
|
None => self.error(format!("no playlist named '{name}'")),
|
||||||
|
|
@ -1283,6 +1314,56 @@ impl App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn save_album(&mut self, name: Option<String>) {
|
||||||
|
if self.view == View::Results
|
||||||
|
&& self.results_title.starts_with("Album: ")
|
||||||
|
&& !self.results.is_empty()
|
||||||
|
{
|
||||||
|
return self.save_results_as_playlist(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(track) = self.artist_target() {
|
||||||
|
if let Some(album) = track.album.clone() {
|
||||||
|
let target_name = name.unwrap_or(album);
|
||||||
|
return self.save_album_background(track, target_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.view == View::Results && !self.results.is_empty() {
|
||||||
|
return self.save_results_as_playlist(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.error("No album to save — open an album or select a song with an album");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_album_background(&mut self, track: Track, target_name: String) {
|
||||||
|
let Some(album) = track.album else {
|
||||||
|
return self.error("No album known for this song");
|
||||||
|
};
|
||||||
|
let artist = split_artists(&track.artist)
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.busy = Some(format!("Saving album \u{201c}{album}\u{201d}"));
|
||||||
|
let tx = self.tx.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let result = (|| -> Result<Vec<Track>, String> {
|
||||||
|
let found = ytmusic::search_collections(&format!("{album} {artist}"), Kind::Album)?;
|
||||||
|
let want = album.to_lowercase();
|
||||||
|
let pick = found
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.title.to_lowercase() == want)
|
||||||
|
.or(found.first())
|
||||||
|
.ok_or_else(|| "album not found".to_string())?;
|
||||||
|
ytmusic::browse_tracks(&pick.browse_id, &pick.title, &pick.artist)
|
||||||
|
})();
|
||||||
|
let _ = tx.send(AppEvent::SavePlaylist {
|
||||||
|
name: target_name,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn start_collection_search(&mut self, query: String, kind: Kind) {
|
fn start_collection_search(&mut self, query: String, kind: Kind) {
|
||||||
let (what, title) = match kind {
|
let (what, title) = match kind {
|
||||||
Kind::Album => ("albums", format!("Albums: {query}")),
|
Kind::Album => ("albums", format!("Albums: {query}")),
|
||||||
|
|
@ -1306,6 +1387,7 @@ impl App {
|
||||||
}
|
}
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc | KeyCode::Char('q') => self.close_collection_pick(),
|
KeyCode::Esc | KeyCode::Char('q') => self.close_collection_pick(),
|
||||||
|
KeyCode::Char('s') | KeyCode::Char('S') => self.save_picked_collection(),
|
||||||
KeyCode::Char('j') | KeyCode::Down => self.pick_sel = (self.pick_sel + 1).min(len - 1),
|
KeyCode::Char('j') | KeyCode::Down => self.pick_sel = (self.pick_sel + 1).min(len - 1),
|
||||||
KeyCode::Char('k') | KeyCode::Up => self.pick_sel = self.pick_sel.saturating_sub(1),
|
KeyCode::Char('k') | KeyCode::Up => self.pick_sel = self.pick_sel.saturating_sub(1),
|
||||||
KeyCode::Char('g') | KeyCode::Home => self.pick_sel = 0,
|
KeyCode::Char('g') | KeyCode::Home => self.pick_sel = 0,
|
||||||
|
|
@ -1340,6 +1422,23 @@ impl App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn save_picked_collection(&mut self) {
|
||||||
|
let c = self.picks.get(self.pick_sel).cloned();
|
||||||
|
self.close_collection_pick();
|
||||||
|
let Some(c) = c else { return };
|
||||||
|
let kind = if c.browse_id.starts_with("VL") { "Playlist" } else { "Album" };
|
||||||
|
let target_name = c.title.clone();
|
||||||
|
self.busy = Some(format!("Saving {kind} \u{201c}{target_name}\u{201d}"));
|
||||||
|
let tx = self.tx.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let result = ytmusic::browse_tracks(&c.browse_id, &c.title, &c.artist);
|
||||||
|
let _ = tx.send(AppEvent::SavePlaylist {
|
||||||
|
name: target_name,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn on_media(&mut self, cmd: Cmd) {
|
fn on_media(&mut self, cmd: Cmd) {
|
||||||
match cmd {
|
match cmd {
|
||||||
Cmd::Play => match self.play_state {
|
Cmd::Play => match self.play_state {
|
||||||
|
|
@ -1492,6 +1591,29 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppEvent::Media(cmd) => self.on_media(cmd),
|
AppEvent::Media(cmd) => self.on_media(cmd),
|
||||||
|
AppEvent::SavePlaylist { name, result } => {
|
||||||
|
self.busy = None;
|
||||||
|
match result {
|
||||||
|
Ok(tracks) if tracks.is_empty() => {
|
||||||
|
self.error(format!("Album \u{201c}{name}\u{201d} contains no tracks"));
|
||||||
|
}
|
||||||
|
Ok(tracks) => {
|
||||||
|
let n = tracks.len();
|
||||||
|
let key = self.store.resolve_exact(&name).unwrap_or(name);
|
||||||
|
self.store.playlists.insert(key.clone(), tracks);
|
||||||
|
if self.persist() {
|
||||||
|
self.info(format!(
|
||||||
|
"Saved {n} tracks to playlist \u{201c}{key}\u{201d}"
|
||||||
|
));
|
||||||
|
let sel = self.selected();
|
||||||
|
self.set_selected(sel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.error(format!("Could not save album \u{201c}{name}\u{201d}: {e}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
AppEvent::Lyrics { id, result } => {
|
AppEvent::Lyrics { id, result } => {
|
||||||
if self.lyrics_id.as_deref() != Some(id.as_str()) {
|
if self.lyrics_id.as_deref() != Some(id.as_str()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -2179,6 +2301,40 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn save_results_as_playlist(&mut self, name: Option<String>) {
|
||||||
|
if self.results.is_empty() {
|
||||||
|
return self.error("No results to save — search or open an album first");
|
||||||
|
}
|
||||||
|
let target_name = match name {
|
||||||
|
Some(n) if !n.trim().is_empty() => n.trim().to_string(),
|
||||||
|
_ => {
|
||||||
|
if let Some(rest) = self.results_title.strip_prefix("Album: ") {
|
||||||
|
rest.trim().to_string()
|
||||||
|
} else if let Some(rest) = self.results_title.strip_prefix("Playlist: ") {
|
||||||
|
rest.trim().to_string()
|
||||||
|
} else if let Some(album) = self.results.iter().find_map(|t| t.album.as_deref()) {
|
||||||
|
album.to_string()
|
||||||
|
} else {
|
||||||
|
return self.error(
|
||||||
|
"No album name found; usage: :savealbum <name> or :saveresults <name>",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let n = self.results.len();
|
||||||
|
let key = self.store.resolve_exact(&target_name).unwrap_or(target_name);
|
||||||
|
self.store
|
||||||
|
.playlists
|
||||||
|
.insert(key.clone(), self.results.clone());
|
||||||
|
if self.persist() {
|
||||||
|
self.info(format!(
|
||||||
|
"Saved {n} tracks to playlist \u{201c}{key}\u{201d}"
|
||||||
|
));
|
||||||
|
let sel = self.selected();
|
||||||
|
self.set_selected(sel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn add_to_playlist(&mut self, name: &str) {
|
fn add_to_playlist(&mut self, name: &str) {
|
||||||
let i = self.selected();
|
let i = self.selected();
|
||||||
let track = match self.view {
|
let track = match self.view {
|
||||||
|
|
@ -2543,6 +2699,46 @@ mod tests {
|
||||||
|
|
||||||
check_playlist_playback(&mut app);
|
check_playlist_playback(&mut app);
|
||||||
|
|
||||||
|
// Test savealbum and saveresults
|
||||||
|
let mut t1 = track("song1111111", "Dreams");
|
||||||
|
t1.album = Some("Rumours".into());
|
||||||
|
let mut t2 = track("song2222222", "Go Your Own Way");
|
||||||
|
t2.album = Some("Rumours".into());
|
||||||
|
app.results = vec![t1.clone(), t2.clone()];
|
||||||
|
app.results_title = "Album: Rumours".into();
|
||||||
|
app.view = View::Results;
|
||||||
|
|
||||||
|
// :savealbum with no args uses album name from results_title
|
||||||
|
app.run_command("savealbum");
|
||||||
|
assert_eq!(app.store.playlists.get("Rumours").map(Vec::len), Some(2));
|
||||||
|
|
||||||
|
// :savealbum with custom name
|
||||||
|
app.run_command(r#"savealbum "Fleetwood Mac - Rumours""#);
|
||||||
|
assert_eq!(
|
||||||
|
app.store
|
||||||
|
.playlists
|
||||||
|
.get("Fleetwood Mac - Rumours")
|
||||||
|
.map(Vec::len),
|
||||||
|
Some(2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// :saveresults with custom name
|
||||||
|
app.run_command(r#"saveresults "Classic Hits""#);
|
||||||
|
assert_eq!(
|
||||||
|
app.store.playlists.get("Classic Hits").map(Vec::len),
|
||||||
|
Some(2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test AppEvent::SavePlaylist background event
|
||||||
|
app.on_event(AppEvent::SavePlaylist {
|
||||||
|
name: "Background Album".into(),
|
||||||
|
result: Ok(vec![track("bg111111111", "BG Track")]),
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
app.store.playlists.get("Background Album").map(Vec::len),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ pub enum Command {
|
||||||
Repeat(Option<RepeatMode>),
|
Repeat(Option<RepeatMode>),
|
||||||
Save(String),
|
Save(String),
|
||||||
SaveForce(String),
|
SaveForce(String),
|
||||||
|
SaveAlbum(Option<String>),
|
||||||
|
SaveResults(Option<String>),
|
||||||
Load(String),
|
Load(String),
|
||||||
Append(String),
|
Append(String),
|
||||||
DeletePlaylist(String),
|
DeletePlaylist(String),
|
||||||
|
|
@ -67,6 +69,8 @@ pub const COMMAND_NAMES: &[&str] = &[
|
||||||
"shuffle",
|
"shuffle",
|
||||||
"repeat",
|
"repeat",
|
||||||
"save",
|
"save",
|
||||||
|
"savealbum",
|
||||||
|
"saveresults",
|
||||||
"load",
|
"load",
|
||||||
"append",
|
"append",
|
||||||
"delpl",
|
"delpl",
|
||||||
|
|
@ -174,6 +178,8 @@ pub fn parse(line: &str) -> Result<Option<Command>, String> {
|
||||||
},
|
},
|
||||||
"save" | "w" => Save(name_arg(arg, "save")?),
|
"save" | "w" => Save(name_arg(arg, "save")?),
|
||||||
"save!" | "w!" => SaveForce(name_arg(arg, "save!")?),
|
"save!" | "w!" => SaveForce(name_arg(arg, "save!")?),
|
||||||
|
"savealbum" | "sa" => SaveAlbum((!arg.is_empty()).then(|| arg.to_string())),
|
||||||
|
"saveresults" | "sr" => SaveResults((!arg.is_empty()).then(|| arg.to_string())),
|
||||||
"load" | "e" => Load(name_arg(arg, "load")?),
|
"load" | "e" => Load(name_arg(arg, "load")?),
|
||||||
"append" => Append(name_arg(arg, "append")?),
|
"append" => Append(name_arg(arg, "append")?),
|
||||||
"delpl" | "deletepl" | "delete" => DeletePlaylist(name_arg(arg, "delpl")?),
|
"delpl" | "deletepl" | "delete" => DeletePlaylist(name_arg(arg, "delpl")?),
|
||||||
|
|
@ -284,5 +290,18 @@ mod tests {
|
||||||
parse("save! my mix"),
|
parse("save! my mix"),
|
||||||
Ok(Some(Command::SaveForce("my mix".into())))
|
Ok(Some(Command::SaveForce("my mix".into())))
|
||||||
);
|
);
|
||||||
|
assert_eq!(parse("savealbum"), Ok(Some(Command::SaveAlbum(None))));
|
||||||
|
assert_eq!(
|
||||||
|
parse(r#"savealbum "Abbey Road""#),
|
||||||
|
Ok(Some(Command::SaveAlbum(Some("Abbey Road".into()))))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("saveresults"),
|
||||||
|
Ok(Some(Command::SaveResults(None)))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse(r#"saveresults "My Hits""#),
|
||||||
|
Ok(Some(Command::SaveResults(Some("My Hits".into()))))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
src/ui.rs
15
src/ui.rs
|
|
@ -806,6 +806,8 @@ const CMD_INFO: &[(&str, &str)] = &[
|
||||||
("shuffle", "toggle shuffle"),
|
("shuffle", "toggle shuffle"),
|
||||||
("repeat", "repeat · off | all | one"),
|
("repeat", "repeat · off | all | one"),
|
||||||
("save", "save the queue as a playlist"),
|
("save", "save the queue as a playlist"),
|
||||||
|
("savealbum", "save album as a playlist · savealbum [name]"),
|
||||||
|
("saveresults", "save results as a playlist · saveresults [name]"),
|
||||||
("load", "load a playlist into the queue"),
|
("load", "load a playlist into the queue"),
|
||||||
("append", "append a playlist to the queue"),
|
("append", "append a playlist to the queue"),
|
||||||
("delpl", "delete a playlist"),
|
("delpl", "delete a playlist"),
|
||||||
|
|
@ -1132,6 +1134,14 @@ const HELP: &[(&str, &str)] = &[
|
||||||
":save n :load n",
|
":save n :load n",
|
||||||
"save the queue as a playlist / load one (:save! n replaces an existing one)",
|
"save the queue as a playlist / load one (:save! n replaces an existing one)",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
":savealbum [n]",
|
||||||
|
"save album or current album results as a playlist",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
":saveresults [n]",
|
||||||
|
"save current results as a playlist",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
":append n :delpl n",
|
":append n :delpl n",
|
||||||
"append a playlist to the queue / delete it",
|
"append a playlist to the queue / delete it",
|
||||||
|
|
@ -1281,7 +1291,10 @@ fn draw_collection_picker(f: &mut Frame, app: &App, area: Rect) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
lines.push(Line::styled(" Enter select · j k move · Esc cancel", dim()));
|
lines.push(Line::styled(
|
||||||
|
" Enter select · s save playlist · j k move · Esc cancel",
|
||||||
|
dim(),
|
||||||
|
));
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Paragraph::new(lines).block(
|
Paragraph::new(lines).block(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue