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)]
|
||||
struct PostQueueQuery {
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
replace: bool,
|
||||
#[serde(default)]
|
||||
next: bool,
|
||||
#[serde(default)]
|
||||
play: bool,
|
||||
}
|
||||
|
||||
async fn post_queue(req: tide::Request<()>) -> tide::Result {
|
||||
let query: PostQueueQuery = req.query()?;
|
||||
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())
|
||||
}
|
||||
|
||||
|
@ -178,7 +199,7 @@ async fn sse(_req: tide::Request<()>, sender: tide::sse::Sender) -> tide::Result
|
|||
#[async_std::main]
|
||||
async fn main() -> tide::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.with_max_level(tracing::Level::WARN)
|
||||
.init();
|
||||
|
||||
let mut app = tide::new();
|
||||
|
|
43
src/mpd.rs
43
src/mpd.rs
|
@ -104,8 +104,8 @@ pub(crate) struct Mpd {
|
|||
}
|
||||
|
||||
impl Mpd {
|
||||
fn escape_str(s: &str) -> String {
|
||||
s.replace("\"", "\\\"").replace("'", "\\'")
|
||||
pub fn escape_str(s: &str) -> String {
|
||||
s.replace('\"', "\\\"").replace('\'', "\\'")
|
||||
}
|
||||
|
||||
pub async fn connect() -> anyhow::Result<Self> {
|
||||
|
@ -130,6 +130,45 @@ impl Mpd {
|
|||
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>> {
|
||||
let mut buffer = String::new();
|
||||
|
||||
|
|
|
@ -32,6 +32,14 @@ button {
|
|||
background-color: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button .material-symbols-outlined {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.browser {
|
||||
|
@ -71,14 +79,6 @@ ul {
|
|||
font-weight: bold;
|
||||
flex: 1;
|
||||
}
|
||||
.queue-clear {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
}
|
||||
.queue-clear .material-symbols-outlined {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.queue {
|
||||
margin-top: 0.5rem;
|
||||
|
@ -112,15 +112,28 @@ ul {
|
|||
-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 {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
background-color: #334;
|
||||
border-radius: .25rem;
|
||||
padding: .75rem 1rem;
|
||||
margin: 16px 16px 0;
|
||||
}
|
||||
@media (prefers-contrast: more) {
|
||||
ul.breadcrumb {
|
||||
|
|
|
@ -1,31 +1,49 @@
|
|||
{# #}
|
||||
<ul class="breadcrumb">
|
||||
<li>
|
||||
<a
|
||||
href="/"
|
||||
hx-replace-url="/"
|
||||
hx-get="/browser"
|
||||
hx-vals='{"path": ""}'
|
||||
hx-target=".browser"
|
||||
>Root</a>
|
||||
</li>
|
||||
{% for (i, component) in path.iter().enumerate() %}
|
||||
<li>
|
||||
{% if i == path.len() - 1 %}
|
||||
{{ component }}
|
||||
{% else %}
|
||||
<a
|
||||
{% let encoded = path[..i + 1].join("/")|urlencode %}
|
||||
href="/?path={{ encoded }}"
|
||||
hx-replace-url="/?path={{ encoded }}"
|
||||
hx-get="/browser"
|
||||
hx-vals='{"path": "{{ encoded }}"}'
|
||||
hx-target=".browser"
|
||||
>{{ component }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="header">
|
||||
<ul class="breadcrumb">
|
||||
<li>
|
||||
<a
|
||||
href="/"
|
||||
hx-replace-url="/"
|
||||
hx-get="/browser"
|
||||
hx-vals='{"path": ""}'
|
||||
hx-target=".browser"
|
||||
>Root</a>
|
||||
</li>
|
||||
{% for (i, component) in path.iter().enumerate() %}
|
||||
<li>
|
||||
{% if i == path.len() - 1 %}
|
||||
{{ component }}
|
||||
{% else %}
|
||||
<a
|
||||
{% let encoded = path[..i + 1].join("/")|urlencode %}
|
||||
href="/?path={{ encoded }}"
|
||||
hx-replace-url="/?path={{ encoded }}"
|
||||
hx-get="/browser"
|
||||
hx-vals='{"path": "{{ encoded }}"}'
|
||||
hx-target=".browser"
|
||||
>{{ component }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% 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">
|
||||
{% for entry in entries %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<div class="queue-header">
|
||||
<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>
|
||||
Clear
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue