Added config support

This commit is contained in:
Connor Johnstone
2026-03-18 15:14:32 -04:00
parent ff41233a96
commit 32b4b533c0
11 changed files with 381 additions and 259 deletions

View File

@@ -1,3 +1,5 @@
use web_sys::HtmlInputElement;
use web_sys::HtmlSelectElement;
use yew::prelude::*;
use crate::api;
@@ -7,6 +9,7 @@ use crate::types::AppConfig;
pub fn settings_page() -> Html {
let config = use_state(|| None::<AppConfig>);
let error = use_state(|| None::<String>);
let message = use_state(|| None::<String>);
{
let config = config.clone();
@@ -21,32 +24,250 @@ pub fn settings_page() -> Html {
});
}
let on_save = {
let config = config.clone();
let message = message.clone();
let error = error.clone();
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
let config = config.clone();
let message = message.clone();
let error = error.clone();
if let Some(ref c) = *config {
let c = c.clone();
wasm_bindgen_futures::spawn_local(async move {
match api::save_config(&c).await {
Ok(updated) => {
config.set(Some(updated));
message.set(Some("Settings saved".to_string()));
error.set(None);
}
Err(e) => error.set(Some(e.0)),
}
});
}
})
};
// Helper to update a field in config state
macro_rules! field_setter {
($config:expr, $field:ident, $val:expr) => {{
let mut c = (*$config).clone().unwrap();
c.$field = $val;
$config.set(Some(c));
}};
}
if let Some(ref err) = *error {
if config.is_none() {
return html! { <div class="error">{ format!("Error: {err}") }</div> };
}
}
let Some(ref c) = *config else {
return html! { <p class="loading">{ "Loading configuration..." }</p> };
};
html! {
<div>
<div class="page-header">
<h2>{ "Settings" }</h2>
</div>
if let Some(ref msg) = *message {
<div class="card" style="border-color: var(--success);">
<p>{ msg }</p>
</div>
}
if let Some(ref err) = *error {
<div class="card error">{ err }</div>
}
{ match &*config {
None => html! { <p class="loading">{ "Loading configuration..." }</p> },
Some(c) => html! {
<div class="card">
<h3>{ "Configuration" }</h3>
<table>
<tbody>
<tr><td class="text-muted">{ "Library Path" }</td><td>{ &c.library_path }</td></tr>
<tr><td class="text-muted">{ "Database URL" }</td><td class="text-sm">{ &c.database_url }</td></tr>
<tr><td class="text-muted">{ "Download Path" }</td><td>{ &c.download_path }</td></tr>
<tr><td class="text-muted">{ "Organization Format" }</td><td><code>{ &c.organization_format }</code></td></tr>
</tbody>
</table>
<form onsubmit={on_save}>
// Paths
<div class="card">
<h3>{ "Paths" }</h3>
<div class="form-group">
<label>{ "Library Path" }</label>
<input type="text" value={c.library_path.clone()}
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
field_setter!(config, library_path, input.value());
})} />
</div>
},
}}
<div class="form-group">
<label>{ "Download Path" }</label>
<input type="text" value={c.download_path.clone()}
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
field_setter!(config, download_path, input.value());
})} />
</div>
<div class="form-group">
<label>{ "Organization Format" }</label>
<input type="text" value={c.organization_format.clone()}
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
field_setter!(config, organization_format, input.value());
})} />
</div>
</div>
// Web Server
<div class="card">
<h3>{ "Web Server" }</h3>
<p class="text-sm text-muted mb-1">{ "Changes require restart" }</p>
<div class="form-group">
<label>{ "Port" }</label>
<input type="number" value={c.web.port.to_string()}
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
if let Ok(port) = input.value().parse() {
let mut cfg = (*config).clone().unwrap();
cfg.web.port = port;
config.set(Some(cfg));
}
})} />
</div>
<div class="form-group">
<label>{ "Bind Address" }</label>
<input type="text" value={c.web.bind.clone()}
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
let mut cfg = (*config).clone().unwrap();
cfg.web.bind = input.value();
config.set(Some(cfg));
})} />
</div>
</div>
// Tagging
<div class="card">
<h3>{ "Tagging" }</h3>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" checked={c.tagging.write_tags}
onchange={let config = config.clone(); Callback::from(move |e: Event| {
let input: HtmlInputElement = e.target_unchecked_into();
let mut cfg = (*config).clone().unwrap();
cfg.tagging.write_tags = input.checked();
config.set(Some(cfg));
})} />
{ " Write tags to files" }
</label>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" checked={c.tagging.auto_tag}
onchange={let config = config.clone(); Callback::from(move |e: Event| {
let input: HtmlInputElement = e.target_unchecked_into();
let mut cfg = (*config).clone().unwrap();
cfg.tagging.auto_tag = input.checked();
config.set(Some(cfg));
})} />
{ " Auto-tag after indexing" }
</label>
</div>
<div class="form-group">
<label>{ "Confidence Threshold" }</label>
<input type="number" step="0.05" min="0" max="1" value={c.tagging.confidence.to_string()}
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
if let Ok(v) = input.value().parse() {
let mut cfg = (*config).clone().unwrap();
cfg.tagging.confidence = v;
config.set(Some(cfg));
}
})} />
</div>
</div>
// Downloads
<div class="card">
<h3>{ "Downloads" }</h3>
<div class="form-group">
<label>{ "Audio Format" }</label>
<select onchange={let config = config.clone(); Callback::from(move |e: Event| {
let select: HtmlSelectElement = e.target_unchecked_into();
let mut cfg = (*config).clone().unwrap();
cfg.download.format = select.value();
config.set(Some(cfg));
})}>
{ for ["opus", "mp3", "flac", "best"].iter().map(|f| html! {
<option value={*f} selected={c.download.format == *f}>{ f }</option>
})}
</select>
</div>
<div class="form-group">
<label>{ "Search Source" }</label>
<select onchange={let config = config.clone(); Callback::from(move |e: Event| {
let select: HtmlSelectElement = e.target_unchecked_into();
let mut cfg = (*config).clone().unwrap();
cfg.download.search_source = select.value();
config.set(Some(cfg));
})}>
{ for [("ytmusic", "YouTube Music"), ("youtube", "YouTube")].iter().map(|(v, label)| html! {
<option value={*v} selected={c.download.search_source == *v}>{ label }</option>
})}
</select>
</div>
<div class="form-group">
<label>{ "Cookies Path (optional)" }</label>
<input type="text" value={c.download.cookies_path.clone().unwrap_or_default()}
placeholder="~/.config/shanty/cookies.txt"
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
let mut cfg = (*config).clone().unwrap();
let v = input.value();
cfg.download.cookies_path = if v.is_empty() { None } else { Some(v) };
config.set(Some(cfg));
})} />
</div>
<div class="form-group">
<label>{ "Rate Limit (requests/hour, guest)" }</label>
<input type="number" value={c.download.rate_limit.to_string()}
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
if let Ok(v) = input.value().parse() {
let mut cfg = (*config).clone().unwrap();
cfg.download.rate_limit = v;
config.set(Some(cfg));
}
})} />
</div>
<div class="form-group">
<label>{ "Rate Limit (requests/hour, with cookies)" }</label>
<input type="number" value={c.download.rate_limit_auth.to_string()}
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
if let Ok(v) = input.value().parse() {
let mut cfg = (*config).clone().unwrap();
cfg.download.rate_limit_auth = v;
config.set(Some(cfg));
}
})} />
</div>
</div>
// Indexing
<div class="card">
<h3>{ "Indexing" }</h3>
<div class="form-group">
<label>{ "Concurrency (file processors)" }</label>
<input type="number" min="1" max="32" value={c.indexing.concurrency.to_string()}
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
if let Ok(v) = input.value().parse() {
let mut cfg = (*config).clone().unwrap();
cfg.indexing.concurrency = v;
config.set(Some(cfg));
}
})} />
</div>
</div>
<button type="submit" class="btn btn-primary">{ "Save Settings" }</button>
</form>
</div>
}
}