feat(plugins): add JSONForms-based plugin configuration UI (#4911)
* feat(plugins): add JSONForms schema for plugin configuration Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance error handling by formatting validation errors with field names Signed-off-by: Deluan <deluan@navidrome.org> * feat: enforce required fields in config validation and improve error handling Signed-off-by: Deluan <deluan@navidrome.org> * format JS code Signed-off-by: Deluan <deluan@navidrome.org> * feat: add config schema validation and enhance manifest structure Signed-off-by: Deluan <deluan@navidrome.org> * feat: refactor plugin config parsing and add unit tests Signed-off-by: Deluan <deluan@navidrome.org> * feat: add config validation error message in Portuguese * feat: enhance AlwaysExpandedArrayLayout with description support and improve array control testing Signed-off-by: Deluan <deluan@navidrome.org> * feat: update Discord Rust plugin configuration to use JSONForm for user tokens and enhance schema validation Signed-off-by: Deluan <deluan@navidrome.org> * fix: resolve React Hooks linting issues in plugin UI components * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * format code Signed-off-by: Deluan <deluan@navidrome.org> * feat: migrate schema validation to use santhosh-tekuri/jsonschema and improve error formatting Signed-off-by: Deluan <deluan@navidrome.org> * address PR comments Signed-off-by: Deluan <deluan@navidrome.org> * fix flaky test Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance array layout and configuration handling with AJV defaults Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement custom tester to exclude enum arrays from AlwaysExpandedArrayLayout Signed-off-by: Deluan <deluan@navidrome.org> * feat: add error boundary for schema rendering and improve error messages Signed-off-by: Deluan <deluan@navidrome.org> * feat: refine non-enum array control logic by utilizing JSONForms schema resolution Signed-off-by: Deluan <deluan@navidrome.org> * feat: add error styling to ToggleEnabledSwitch for disabled state Signed-off-by: Deluan <deluan@navidrome.org> * feat: adjust label positioning and styling in SchemaConfigEditor for improved layout Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement outlined input controls renderers to replace custom fragile CSS Signed-off-by: Deluan <deluan@navidrome.org> * feat: remove margin from last form control inside array items for better spacing Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance AJV error handling to transform required errors for field-level validation Signed-off-by: Deluan <deluan@navidrome.org> * feat: set default value for User Tokens in manifest.json to improve user experience Signed-off-by: Deluan <deluan@navidrome.org> * format Signed-off-by: Deluan <deluan@navidrome.org> * feat: add margin to outlined input controls for improved spacing Signed-off-by: Deluan <deluan@navidrome.org> * feat: remove redundant margin rule for last form control in array items Signed-off-by: Deluan <deluan@navidrome.org> * feat: adjust font size of label elements in SchemaConfigEditor for improved readability Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -25,5 +25,74 @@
|
||||
"artwork": {
|
||||
"reason": "To get track artwork URLs for rich presence display"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clientid": {
|
||||
"type": "string",
|
||||
"title": "Discord Application Client ID",
|
||||
"description": "The Client ID from your Discord Developer Application. Create one at https://discord.com/developers/applications",
|
||||
"minLength": 17,
|
||||
"maxLength": 20,
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
"users": {
|
||||
"type": "array",
|
||||
"title": "User Tokens",
|
||||
"description": "Discord tokens for each Navidrome user. WARNING: Store tokens securely!",
|
||||
"default": [{}],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"title": "Navidrome Username",
|
||||
"description": "The Navidrome username to associate with this Discord token",
|
||||
"minLength": 1
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"title": "Discord Token",
|
||||
"description": "The user's Discord token (keep this secret!)",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"required": ["username", "token"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["clientid"]
|
||||
},
|
||||
"uiSchema": {
|
||||
"type": "VerticalLayout",
|
||||
"elements": [
|
||||
{
|
||||
"type": "Control",
|
||||
"scope": "#/properties/clientid"
|
||||
},
|
||||
{
|
||||
"type": "Control",
|
||||
"scope": "#/properties/users",
|
||||
"options": {
|
||||
"elementLabelProp": "username",
|
||||
"detail": {
|
||||
"type": "HorizontalLayout",
|
||||
"elements": [
|
||||
{
|
||||
"type": "Control",
|
||||
"scope": "#/properties/username"
|
||||
},
|
||||
{
|
||||
"type": "Control",
|
||||
"scope": "#/properties/token"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,9 @@
|
||||
//!
|
||||
//! ## Configuration
|
||||
//!
|
||||
//! ```toml
|
||||
//! [PluginConfig.discord-rich-presence-rs]
|
||||
//! clientid = "YOUR_DISCORD_APPLICATION_ID"
|
||||
//! "user.username1" = "discord_token1"
|
||||
//! "user.username2" = "discord_token2"
|
||||
//! ```
|
||||
//! Configure this plugin through the Navidrome UI with:
|
||||
//! - Discord Application Client ID
|
||||
//! - User tokens array mapping Navidrome usernames to Discord tokens
|
||||
//!
|
||||
//! **WARNING**: This plugin is for demonstration purposes only. Storing Discord tokens
|
||||
//! in configuration files is not secure and may violate Discord's terms of service.
|
||||
@@ -32,6 +29,7 @@ use nd_pdk::websocket::{
|
||||
OnBinaryMessageRequest, OnCloseRequest, OnErrorRequest, OnTextMessageRequest,
|
||||
TextMessageProvider,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
mod rpc;
|
||||
|
||||
@@ -48,7 +46,7 @@ nd_pdk::register_websocket_close!(DiscordPlugin);
|
||||
// ============================================================================
|
||||
|
||||
const CLIENT_ID_KEY: &str = "clientid";
|
||||
const USER_KEY_PREFIX: &str = "user.";
|
||||
const USERS_KEY: &str = "users";
|
||||
const PAYLOAD_HEARTBEAT: &str = "heartbeat";
|
||||
const PAYLOAD_CLEAR_ACTIVITY: &str = "clear-activity";
|
||||
|
||||
@@ -64,19 +62,31 @@ struct DiscordPlugin;
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
|
||||
/// User token entry from the config schema
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UserToken {
|
||||
username: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
fn get_config() -> Result<(String, std::collections::HashMap<String, String>), Error> {
|
||||
let client_id = config::get(CLIENT_ID_KEY)?
|
||||
.filter(|s| !s.is_empty())
|
||||
.ok_or_else(|| Error::msg("missing clientid in configuration"))?;
|
||||
|
||||
// Get all user keys with the "user." prefix
|
||||
let user_keys = config::keys(USER_KEY_PREFIX)?;
|
||||
|
||||
// Get users array from config (JSON format)
|
||||
let users_json = config::get(USERS_KEY)?.unwrap_or_default();
|
||||
|
||||
let mut users = std::collections::HashMap::new();
|
||||
for key in user_keys {
|
||||
let username = key.strip_prefix(USER_KEY_PREFIX).unwrap_or(&key);
|
||||
if let Some(token) = config::get(&key)?.filter(|s| !s.is_empty()) {
|
||||
users.insert(username.to_string(), token);
|
||||
if !users_json.is_empty() {
|
||||
// Parse JSON array of user tokens
|
||||
let user_tokens: Vec<UserToken> = serde_json::from_str(&users_json)
|
||||
.map_err(|e| Error::msg(format!("failed to parse users config: {}", e)))?;
|
||||
|
||||
for user_token in user_tokens {
|
||||
if !user_token.username.is_empty() && !user_token.token.is_empty() {
|
||||
users.insert(user_token.username, user_token.token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user