feat(users): per-user library assignment + themed confirm modal (#222)

Adds Navidrome library assignment to the User Management settings
panel: GET /api/library + PUT /api/user/{id}/library wired through
new Rust commands. UserForm gets a checkbox picker (hidden for
admins, who auto-receive all libraries server-side) with inline
validation. User rows compacted to a single clickable line with
hover highlight; native confirm() replaced by a portal-based
ConfirmModal (theme-aware, ESC/Enter, vertically centered).

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Frank Stellmacher
2026-04-20 22:18:10 +02:00
committed by GitHub
parent 6d3c50264a
commit 459c9f688d
13 changed files with 392 additions and 66 deletions
+45
View File
@@ -385,6 +385,49 @@ async fn nd_delete_user(
Ok(())
}
/// GET `/api/library` — list all libraries (admin only). Returns the raw JSON array.
#[tauri::command]
async fn nd_list_libraries(
server_url: String,
token: String,
) -> Result<serde_json::Value, String> {
let resp = reqwest::Client::new()
.get(format!("{}/api/library", server_url))
.header("X-ND-Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
return Err(format!("HTTP {}", resp.status()));
}
resp.json::<serde_json::Value>().await.map_err(|e| e.to_string())
}
/// PUT `/api/user/{id}/library` — assign libraries to a non-admin user.
/// Admin users auto-receive all libraries; calling this for an admin returns HTTP 400.
#[tauri::command]
async fn nd_set_user_libraries(
server_url: String,
token: String,
id: String,
library_ids: Vec<i64>,
) -> Result<(), String> {
let body = serde_json::json!({ "libraryIds": library_ids });
let resp = reqwest::Client::new()
.put(format!("{}/api/user/{}/library", server_url, id))
.header("X-ND-Authorization", format!("Bearer {}", token))
.json(&body)
.send()
.await
.map_err(|e| e.to_string())?;
let status = resp.status();
if !status.is_success() {
let text = resp.text().await.unwrap_or_default();
return Err(format!("HTTP {}: {}", status, text));
}
Ok(())
}
const RADIO_PAGE_SIZE: u32 = 25;
/// Search the radio-browser.info directory (needs User-Agent header — CORS would block WebView).
@@ -3342,6 +3385,8 @@ pub fn run() {
nd_create_user,
nd_update_user,
nd_delete_user,
nd_list_libraries,
nd_set_user_libraries,
search_radio_browser,
get_top_radio_stations,
fetch_url_bytes,