Added directory playlist and simple queue management
parent
2d142e0657
commit
6089e9082a
70
src/app.rs
70
src/app.rs
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{path::PathBuf, time::Duration};
|
use std::{path::PathBuf, time::Duration};
|
||||||
|
|
||||||
use rmp::{QueuePlaylist, TrackChangeOptions};
|
use rmp::{DirectoryPlaylist, PlaylistElement, QueuePlaylist, StatefulList, TrackChangeOptions};
|
||||||
|
|
||||||
use crate::playback::Playback;
|
use crate::playback::Playback;
|
||||||
|
|
||||||
|
|
@ -14,18 +14,45 @@ pub struct App {
|
||||||
pub should_quit: bool,
|
pub should_quit: bool,
|
||||||
pub playback: Playback,
|
pub playback: Playback,
|
||||||
pub track_change_options: TrackChangeOptions,
|
pub track_change_options: TrackChangeOptions,
|
||||||
|
pub focus: AppWindow,
|
||||||
|
pub directory_playlist: DirectoryPlaylist,
|
||||||
pub queue_playlist: QueuePlaylist,
|
pub queue_playlist: QueuePlaylist,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub enum AppWindow {
|
||||||
|
#[default]
|
||||||
|
DirectoryPlaylist,
|
||||||
|
QueuePlaylist,
|
||||||
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(options: AppOptions) -> Self {
|
pub fn new(options: AppOptions) -> Self {
|
||||||
Self {
|
Self {
|
||||||
options,
|
options,
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
playback: Playback::new(),
|
playback: Playback::new(),
|
||||||
// TODO: Maybe read from some configuration (args, file, etc...)
|
|
||||||
track_change_options: Default::default(),
|
track_change_options: Default::default(),
|
||||||
queue_playlist: QueuePlaylist::generate_mock(),
|
focus: Default::default(),
|
||||||
|
directory_playlist: DirectoryPlaylist::read_directory(
|
||||||
|
format!("{}/Music/Ultra Bra", std::env::var("HOME").unwrap()).into(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
queue_playlist: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focused_window(&self) -> &StatefulList<PlaylistElement> {
|
||||||
|
match &self.focus {
|
||||||
|
AppWindow::DirectoryPlaylist => &self.directory_playlist.playlist,
|
||||||
|
AppWindow::QueuePlaylist => &self.queue_playlist.playlist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focused_window_mut(&mut self) -> &mut StatefulList<PlaylistElement> {
|
||||||
|
match &mut self.focus {
|
||||||
|
AppWindow::DirectoryPlaylist => &mut self.directory_playlist.playlist,
|
||||||
|
AppWindow::QueuePlaylist => &mut self.queue_playlist.playlist,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,12 +76,32 @@ impl App {
|
||||||
self.playback.play_immediate(track);
|
self.playback.play_immediate(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_selected_to_queue(&mut self) {
|
||||||
|
// TODO: Fix this mess, add some arguments
|
||||||
|
if let AppWindow::DirectoryPlaylist = self.focus {
|
||||||
|
if let Some(selected) = self.directory_playlist.playlist.current() {
|
||||||
|
self.queue_playlist.playlist.items.push(selected.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_selected_from_queue(&mut self) {
|
||||||
|
// TODO: Fix this mess, add some arguments
|
||||||
|
if let AppWindow::QueuePlaylist = self.focus {
|
||||||
|
if let Some(cursor) = self.queue_playlist.playlist.state.selected() {
|
||||||
|
self.queue_playlist.playlist.items.remove(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_key(&mut self, key: char) {
|
pub fn on_key(&mut self, key: char) {
|
||||||
match key {
|
match key {
|
||||||
'S' => self.toggle_shuffle(),
|
'S' => self.toggle_shuffle(),
|
||||||
'X' => self.toggle_next(),
|
'X' => self.toggle_next(),
|
||||||
'R' => self.toggle_repeat(),
|
'R' => self.toggle_repeat(),
|
||||||
' ' => self.toggle_pause(),
|
' ' => self.toggle_pause(),
|
||||||
|
'a' => self.add_selected_to_queue(),
|
||||||
|
'd' => self.remove_selected_from_queue(),
|
||||||
'q' => self.should_quit = true,
|
'q' => self.should_quit = true,
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
@ -65,23 +112,26 @@ impl App {
|
||||||
pub fn on_right(&mut self) {}
|
pub fn on_right(&mut self) {}
|
||||||
|
|
||||||
pub fn on_up(&mut self) {
|
pub fn on_up(&mut self) {
|
||||||
// TODO: Apply to selected playlist
|
self.focused_window_mut().previous();
|
||||||
self.queue_playlist.playlist.previous();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_down(&mut self) {
|
pub fn on_down(&mut self) {
|
||||||
// TODO: Apply to selected playlist
|
self.focused_window_mut().next();
|
||||||
self.queue_playlist.playlist.next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_enter(&mut self) {
|
pub fn on_enter(&mut self) {
|
||||||
// TODO: Apply to selected playlist
|
// TODO: Handle directory navigation
|
||||||
if let Some(current) = self.queue_playlist.playlist.current() {
|
if let Some(current) = self.focused_window().current() {
|
||||||
self.play(current.into());
|
self.play(current.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_tab(&mut self) {}
|
pub fn on_tab(&mut self) {
|
||||||
|
self.focus = match self.focus {
|
||||||
|
AppWindow::QueuePlaylist => AppWindow::DirectoryPlaylist,
|
||||||
|
AppWindow::DirectoryPlaylist => AppWindow::QueuePlaylist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_tick(&mut self, _duration: Duration) {}
|
pub fn on_tick(&mut self, _duration: Duration) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
src/lib.rs
33
src/lib.rs
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{ops::Deref, path::PathBuf};
|
use std::{error::Error, ops::Deref, path::PathBuf};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct ListState {
|
pub struct ListState {
|
||||||
|
|
@ -112,28 +112,25 @@ pub struct DirectoryPlaylist {
|
||||||
pub playlist: StatefulList<PlaylistElement>,
|
pub playlist: StatefulList<PlaylistElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DirectoryPlaylist {
|
||||||
|
pub fn read_directory(path: PathBuf) -> Result<Self, Box<dyn Error>> {
|
||||||
|
let mut playlist: Vec<PlaylistElement> = vec![];
|
||||||
|
for entry in std::fs::read_dir(&path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
playlist.push(entry.path());
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
directory: path,
|
||||||
|
playlist: StatefulList::with_items(playlist),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct QueuePlaylist {
|
pub struct QueuePlaylist {
|
||||||
pub playlist: StatefulList<PlaylistElement>,
|
pub playlist: StatefulList<PlaylistElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QueuePlaylist {
|
|
||||||
pub fn generate_mock() -> Self {
|
|
||||||
Self {
|
|
||||||
playlist: StatefulList::with_items(vec![
|
|
||||||
"/home/hheikkinen/Music/Mariya Takeuchi/Mariya Takeuchi (竹内 まりや) - September [Lyrics Kan⧸Rom⧸Eng] [3wkxIBBP7Vg].opus",
|
|
||||||
"/home/hheikkinen/Music/Compilerbau - Le Jardin/LEJARDIN.OGG",
|
|
||||||
"/home/hheikkinen/Music/Casiopea - Mint Jams (1982) FULL ALBUM/001 Take Me.opus",
|
|
||||||
"/home/hheikkinen/Music/Casiopea - Mint Jams (1982) FULL ALBUM/002 Asayake.opus",
|
|
||||||
"/home/hheikkinen/Music/A Groovy Thing/01 - Flamingosis - A Groovy Intro.mp3",
|
|
||||||
"/home/hheikkinen/Music/Brian Ellis - Smocaine 3/Brian Ellis - Smocaine 3- An MDE Film OST - 01 Smocaine Theme (TV Edit).wav",
|
|
||||||
"/home/hheikkinen/Music/Jun Fukamachi - Starview HCT-5808/T-02.m4a",
|
|
||||||
"/home/hheikkinen/Music/noby/Fluidy [189046035].flac",
|
|
||||||
].iter().map(|val| val.into()).collect()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TrackChangeOptions {
|
pub struct TrackChangeOptions {
|
||||||
pub shuffle: bool,
|
pub shuffle: bool,
|
||||||
|
|
|
||||||
55
src/ui.rs
55
src/ui.rs
|
|
@ -2,15 +2,16 @@ use ratatui::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
widgets::{block::Title, *},
|
widgets::{block::Title, *},
|
||||||
};
|
};
|
||||||
|
use rmp::{PlaylistElement, StatefulList};
|
||||||
|
|
||||||
use super::app::App;
|
use super::app::App;
|
||||||
|
|
||||||
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||||
let chunks = Layout::default()
|
let main_chunks = Layout::default()
|
||||||
.constraints([Constraint::Min(3), Constraint::Length(2)].as_ref())
|
.constraints([Constraint::Min(3), Constraint::Length(2)].as_ref())
|
||||||
.split(f.size());
|
.split(f.size());
|
||||||
draw_playlist(f, app, chunks[0]);
|
draw_playlists(f, app, main_chunks[0]);
|
||||||
draw_player(f, app, chunks[1]);
|
draw_player(f, app, main_chunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static PRIMARY_COLOR: Color = Color::Rgb(200, 150, 70);
|
static PRIMARY_COLOR: Color = Color::Rgb(200, 150, 70);
|
||||||
|
|
@ -19,17 +20,40 @@ static SECONDARY_COLOR: Color = Color::Rgb(200, 200, 200);
|
||||||
static PRIMARY_CONTRAST: Color = Color::Black;
|
static PRIMARY_CONTRAST: Color = Color::Black;
|
||||||
static CLEAR_CONTRAST: Color = Color::Rgb(100, 100, 100);
|
static CLEAR_CONTRAST: Color = Color::Rgb(100, 100, 100);
|
||||||
|
|
||||||
fn draw_playlist<B: Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
|
fn draw_playlists<B: Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
|
||||||
let tracks: Vec<_> = app
|
let layout = Layout::default()
|
||||||
.queue_playlist
|
.direction(Direction::Horizontal)
|
||||||
.playlist
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
|
.split(area);
|
||||||
|
draw_playlist(
|
||||||
|
f,
|
||||||
|
app,
|
||||||
|
layout[0],
|
||||||
|
app.directory_playlist.directory.to_string_lossy().into(),
|
||||||
|
&app.directory_playlist.playlist,
|
||||||
|
);
|
||||||
|
draw_playlist(
|
||||||
|
f,
|
||||||
|
app,
|
||||||
|
layout[1],
|
||||||
|
"queue".into(),
|
||||||
|
&app.queue_playlist.playlist,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_playlist<B: Backend>(
|
||||||
|
f: &mut Frame<B>,
|
||||||
|
app: &App,
|
||||||
|
area: Rect,
|
||||||
|
title: String,
|
||||||
|
playlist: &StatefulList<PlaylistElement>,
|
||||||
|
) {
|
||||||
|
let tracks: Vec<_> = playlist
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, path)| {
|
.map(|(index, path)| {
|
||||||
let selected = app
|
let selected = playlist
|
||||||
.queue_playlist
|
|
||||||
.playlist
|
|
||||||
.state
|
.state
|
||||||
.selected()
|
.selected()
|
||||||
.map_or(false, |selected| index == selected);
|
.map_or(false, |selected| index == selected);
|
||||||
|
|
@ -51,13 +75,14 @@ fn draw_playlist<B: Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
|
||||||
}
|
}
|
||||||
(_, _) => (),
|
(_, _) => (),
|
||||||
}
|
}
|
||||||
let content = Span::from(path.to_string_lossy().to_string());
|
let content = Span::from(render_track_name(path));
|
||||||
ListItem::new(content).set_style(style)
|
ListItem::new(content).set_style(style)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let playlist = List::new(tracks)
|
let playlist = List::new(tracks)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
|
.title(title)
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(SECONDARY_COLOR)),
|
.border_style(Style::default().fg(SECONDARY_COLOR)),
|
||||||
)
|
)
|
||||||
|
|
@ -127,3 +152,11 @@ fn draw_player<B: Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
|
||||||
|
|
||||||
f.render_widget(player, area)
|
f.render_widget(player, area)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_track_name(track: &PlaylistElement) -> String {
|
||||||
|
track
|
||||||
|
.file_name()
|
||||||
|
.map_or("<unspeakable file name>".into(), |os_str| {
|
||||||
|
os_str.to_string_lossy().to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue