Add buttons to play/queue an entire folder
All checks were successful
continuous-integration/drone/push Build is passing

Fixes #2
This commit is contained in:
Sijmen 2023-05-07 01:50:06 +02:00
parent 7a1adb37b2
commit c4936dc4ff
Signed by: vijfhoek
GPG key ID: DAF7821E067D9C48
5 changed files with 136 additions and 45 deletions

View file

@ -92,12 +92,33 @@ async fn get_browser(req: tide::Request<()>) -> tide::Result {
#[derive(Deserialize)] #[derive(Deserialize)]
struct PostQueueQuery { struct PostQueueQuery {
path: String, path: String,
#[serde(default)]
replace: bool,
#[serde(default)]
next: bool,
#[serde(default)]
play: bool,
} }
async fn post_queue(req: tide::Request<()>) -> tide::Result { async fn post_queue(req: tide::Request<()>) -> tide::Result {
let query: PostQueueQuery = req.query()?; let query: PostQueueQuery = req.query()?;
let path = percent_decode_str(&query.path).decode_utf8_lossy(); let path = percent_decode_str(&query.path).decode_utf8_lossy();
mpd::connect()?.add(&path)?; let mut mpd = mpd::Mpd::connect().await?;
if query.replace {
mpd.clear().await?;
}
if query.next {
mpd.add_pos(&path, "+0").await?;
} else {
mpd.add(&path).await?;
}
if query.play {
mpd.play().await?;
}
Ok("".into()) Ok("".into())
} }
@ -178,7 +199,7 @@ async fn sse(_req: tide::Request<()>, sender: tide::sse::Sender) -> tide::Result
#[async_std::main] #[async_std::main]
async fn main() -> tide::Result<()> { async fn main() -> tide::Result<()> {
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO) .with_max_level(tracing::Level::WARN)
.init(); .init();
let mut app = tide::new(); let mut app = tide::new();

View file

@ -104,8 +104,8 @@ pub(crate) struct Mpd {
} }
impl Mpd { impl Mpd {
fn escape_str(s: &str) -> String { pub fn escape_str(s: &str) -> String {
s.replace("\"", "\\\"").replace("'", "\\'") s.replace('\"', "\\\"").replace('\'', "\\'")
} }
pub async fn connect() -> anyhow::Result<Self> { pub async fn connect() -> anyhow::Result<Self> {
@ -130,6 +130,45 @@ impl Mpd {
Ok(Self { stream, reader }) Ok(Self { stream, reader })
} }
pub async fn command(&mut self, command: &str) -> anyhow::Result<()> {
self.stream
.write_all(format!("{command}\n").as_bytes())
.await?;
let mut buffer = String::new();
loop {
buffer.clear();
self.reader.read_line(&mut buffer).await?;
let split: Vec<_> = buffer.trim_end().split_ascii_whitespace().collect();
if split[0] == "OK" {
break Ok(());
} else if split[0] == "ACK" {
break Err(anyhow!(buffer));
}
}
}
pub async fn clear(&mut self) -> anyhow::Result<()> {
self.command("clear").await
}
pub async fn add(&mut self, path: &str) -> anyhow::Result<()> {
let path = Self::escape_str(path);
self.command(&format!("add \"{path}\"")).await
}
pub async fn add_pos(&mut self, path: &str, pos: &str) -> anyhow::Result<()> {
let path = Self::escape_str(path);
let pos = Self::escape_str(pos);
self.command(&format!("add \"{path}\" \"{pos}\"")).await
}
pub async fn play(&mut self) -> anyhow::Result<()> {
self.command("play").await
}
pub(crate) async fn idle(&mut self, systems: &[&str]) -> anyhow::Result<Vec<String>> { pub(crate) async fn idle(&mut self, systems: &[&str]) -> anyhow::Result<Vec<String>> {
let mut buffer = String::new(); let mut buffer = String::new();

View file

@ -32,6 +32,14 @@ button {
background-color: transparent; background-color: transparent;
border: none; border: none;
color: inherit; color: inherit;
padding: 0;
font-weight: bold;
display: flex;
line-height: 24px;
cursor: pointer;
}
button .material-symbols-outlined {
margin-right: 0.25rem;
} }
.browser { .browser {
@ -71,14 +79,6 @@ ul {
font-weight: bold; font-weight: bold;
flex: 1; flex: 1;
} }
.queue-clear {
font-weight: bold;
display: flex;
line-height: 24px;
}
.queue-clear .material-symbols-outlined {
margin-right: 0.25rem;
}
.queue { .queue {
margin-top: 0.5rem; margin-top: 0.5rem;
@ -112,15 +112,28 @@ ul {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.browser .header {
display: flex;
flex-flow: column;
background-color: #334;
border-radius: 0.25rem;
padding: 0.75rem 1rem;
margin: 16px 16px 0;
}
.browser .buttons {
display: flex;
flex-flow: row;
margin-top: 0.75rem;
}
.browser .buttons button {
margin-right: 1.0rem;
}
ul.breadcrumb { ul.breadcrumb {
display: flex; display: flex;
margin-bottom: 1rem;
flex-wrap: wrap; flex-wrap: wrap;
list-style: none; list-style: none;
background-color: #334;
border-radius: .25rem;
padding: .75rem 1rem;
margin: 16px 16px 0;
} }
@media (prefers-contrast: more) { @media (prefers-contrast: more) {
ul.breadcrumb { ul.breadcrumb {

View file

@ -1,31 +1,49 @@
{# #} {# #}
<ul class="breadcrumb"> <div class="header">
<li> <ul class="breadcrumb">
<a <li>
href="/" <a
hx-replace-url="/" href="/"
hx-get="/browser" hx-replace-url="/"
hx-vals='{"path": ""}' hx-get="/browser"
hx-target=".browser" hx-vals='{"path": ""}'
>Root</a> hx-target=".browser"
</li> >Root</a>
{% for (i, component) in path.iter().enumerate() %} </li>
<li> {% for (i, component) in path.iter().enumerate() %}
{% if i == path.len() - 1 %} <li>
{{ component }} {% if i == path.len() - 1 %}
{% else %} {{ component }}
<a {% else %}
{% let encoded = path[..i + 1].join("/")|urlencode %} <a
href="/?path={{ encoded }}" {% let encoded = path[..i + 1].join("/")|urlencode %}
hx-replace-url="/?path={{ encoded }}" href="/?path={{ encoded }}"
hx-get="/browser" hx-replace-url="/?path={{ encoded }}"
hx-vals='{"path": "{{ encoded }}"}' hx-get="/browser"
hx-target=".browser" hx-vals='{"path": "{{ encoded }}"}'
>{{ component }}</a> hx-target=".browser"
{% endif %} >{{ component }}</a>
</li> {% endif %}
{% endfor %} </li>
</ul> {% endfor %}
</ul>
<div class="buttons">
{% let encoded = path.join("/")|urlencode %}
<button hx-delete="/queue" hx-swap="none" hx-post="/queue?path={{ encoded }}">
<span class="material-symbols-outlined">playlist_add</span>
Queue all
</button>
<button hx-delete="/queue" hx-swap="none" hx-post="/queue?path={{ encoded }}&replace=true&play=true">
<span class="material-symbols-outlined">playlist_play</span>
Play all
</button>
<button hx-delete="/queue" hx-swap="none" hx-post="/queue?path={{ encoded }}&next=true">
<span class="material-symbols-outlined">playlist_add</span>
Play next
</button>
</div>
</div>
<ul class="dir" hx-boost="true" tabindex="-1"> <ul class="dir" hx-boost="true" tabindex="-1">
{% for entry in entries %} {% for entry in entries %}

View file

@ -3,7 +3,7 @@
<div class="queue-header"> <div class="queue-header">
<div class="queue-next">Next in queue</div> <div class="queue-next">Next in queue</div>
<button class="queue-clear" role="button" hx-delete="/queue" hx-swap="none"> <button class="queue-clear" hx-delete="/queue" hx-swap="none">
<span class="material-symbols-outlined">playlist_remove</span> <span class="material-symbols-outlined">playlist_remove</span>
Clear Clear
</div> </div>