Add buttons to play/queue an entire folder
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Fixes #2
This commit is contained in:
parent
7a1adb37b2
commit
c4936dc4ff
5 changed files with 136 additions and 45 deletions
25
src/main.rs
25
src/main.rs
|
@ -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();
|
||||||
|
|
43
src/mpd.rs
43
src/mpd.rs
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue