Minimal subsonic functionality

This commit is contained in:
Connor Johnstone
2026-03-20 20:04:35 -04:00
parent abe321a317
commit 621355e352
19 changed files with 2107 additions and 22 deletions

View File

@@ -3,7 +3,7 @@ use web_sys::HtmlSelectElement;
use yew::prelude::*;
use crate::api;
use crate::types::{AppConfig, YtAuthStatus};
use crate::types::{AppConfig, SubsonicPasswordStatus, YtAuthStatus};
#[function_component(SettingsPage)]
pub fn settings_page() -> Html {
@@ -12,11 +12,15 @@ pub fn settings_page() -> Html {
let message = use_state(|| None::<String>);
let ytauth = use_state(|| None::<YtAuthStatus>);
let ytauth_loading = use_state(|| false);
let subsonic_status = use_state(|| None::<SubsonicPasswordStatus>);
let subsonic_password = use_state(String::new);
let subsonic_saving = use_state(|| false);
{
let config = config.clone();
let error = error.clone();
let ytauth = ytauth.clone();
let subsonic_status = subsonic_status.clone();
use_effect_with((), move |_| {
wasm_bindgen_futures::spawn_local(async move {
match api::get_config().await {
@@ -29,6 +33,11 @@ pub fn settings_page() -> Html {
ytauth.set(Some(status));
}
});
wasm_bindgen_futures::spawn_local(async move {
if let Ok(status) = api::get_subsonic_password_status().await {
subsonic_status.set(Some(status));
}
});
});
}
@@ -500,6 +509,93 @@ pub fn settings_page() -> Html {
{ ytauth_html }
</div>
// Subsonic API
<div class="card">
<h3>{ "Subsonic API" }</h3>
<p class="text-sm text-muted mb-1">
{ "Connect Subsonic-compatible apps (DSub, Symfonium, Feishin) to stream your library. " }
{ "This is a minimal Subsonic implementation for basic browsing and playback. " }
{ "For a full-featured Subsonic server, consider " }
<a href="https://www.navidrome.org" target="_blank">{ "Navidrome" }</a>
{ " pointed at the same library." }
</p>
<div class="form-group">
<label>{ "Server URL (most clients add /rest automatically)" }</label>
<input type="text" readonly=true value={
format!("http://{}:{}",
c.web.bind.clone(),
c.web.port)
} />
</div>
{
if let Some(ref status) = *subsonic_status {
if status.set {
html! {
<p class="text-sm">
<span class="badge badge-success">{ "Password set" }</span>
</p>
}
} else {
html! {
<p class="text-sm text-muted">{ "No Subsonic password set. Set one below to enable access." }</p>
}
}
} else {
html! { <p class="text-sm text-muted">{ "Loading..." }</p> }
}
}
<div class="card" style="border-color: var(--warning); background: rgba(234, 179, 8, 0.08); margin: 0.75rem 0;">
<p class="text-sm" style="margin:0;">
<strong style="color: var(--warning);">{ "Warning: " }</strong>
{ "This password is stored in plaintext per the Subsonic protocol. Do " }
<strong>{ "not" }</strong>
{ " reuse a password from another account." }
</p>
</div>
<div class="form-group">
<label>{ "Subsonic Password" }</label>
<input type="password" placeholder="Enter Subsonic password"
value={(*subsonic_password).clone()}
oninput={let subsonic_password = subsonic_password.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
subsonic_password.set(input.value());
})} />
</div>
<button type="button" class="btn btn-primary"
disabled={*subsonic_saving || subsonic_password.is_empty()}
onclick={{
let subsonic_password = subsonic_password.clone();
let subsonic_saving = subsonic_saving.clone();
let subsonic_status = subsonic_status.clone();
let message = message.clone();
let error = error.clone();
Callback::from(move |_: MouseEvent| {
let pw = (*subsonic_password).clone();
let subsonic_saving = subsonic_saving.clone();
let subsonic_status = subsonic_status.clone();
let subsonic_password = subsonic_password.clone();
let message = message.clone();
let error = error.clone();
subsonic_saving.set(true);
wasm_bindgen_futures::spawn_local(async move {
match api::set_subsonic_password(&pw).await {
Ok(_) => {
message.set(Some("Subsonic password saved".into()));
subsonic_password.set(String::new());
if let Ok(s) = api::get_subsonic_password_status().await {
subsonic_status.set(Some(s));
}
}
Err(e) => error.set(Some(e.0)),
}
subsonic_saving.set(false);
});
})
}}>
{ if *subsonic_saving { "Saving..." } else { "Save Subsonic Password" } }
</button>
</div>
// Metadata Providers
<div class="card">
<h3>{ "Metadata Providers" }</h3>