*/ public static function defaults() { $defaults = array( 'default_theme' => self::get_default_preset_slug(), 'enabled_themes' => array_keys( self::get_design_presets() ), ); foreach ( self::get_design_field_keys() as $field_key ) { $defaults[ $field_key ] = ''; } return $defaults; } /** * Get current settings merged with defaults. * * @return array */ public static function get() { $settings = get_option( MAP_OPTION_KEY, array() ); if ( ! is_array( $settings ) ) { $settings = array(); } $settings = self::migrate_legacy_settings( $settings ); return wp_parse_args( $settings, self::defaults() ); } /** * Get the centralized design settings schema. * * @return array> */ public static function get_design_schema() { return array( 'presets' => array( 'title' => __( 'Presets', 'modern-audio-player' ), 'description' => __( 'Choose the default player preset and which presets can be selected in blocks or shortcodes.', 'modern-audio-player' ), 'fields' => array( 'default_theme' => array( 'label' => __( 'Default preset', 'modern-audio-player' ), 'type' => 'select', 'description' => __( 'Used when a block or shortcode inherits the global design preset.', 'modern-audio-player' ), 'options' => self::get_preset_options(), ), 'enabled_themes' => array( 'label' => __( 'Available presets', 'modern-audio-player' ), 'type' => 'checkbox_group', 'description' => __( 'Controls which presets editors can choose per player instance.', 'modern-audio-player' ), 'options' => self::get_preset_options(), ), ), ), 'colors' => array( 'title' => __( 'Colors', 'modern-audio-player' ), 'description' => __( 'Core palette tokens output to CSS variables for every rendered player.', 'modern-audio-player' ), 'fields' => array( 'player_background_color' => array( 'label' => __( 'Player background', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Primary background tone behind the player card.', 'modern-audio-player' ), ), 'player_surface_color' => array( 'label' => __( 'Surface color', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Secondary surface used for cover wells and layered areas.', 'modern-audio-player' ), ), 'player_text_color' => array( 'label' => __( 'Primary text', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Main title and control text color.', 'modern-audio-player' ), ), 'player_muted_text_color' => array( 'label' => __( 'Muted text', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Secondary metadata and timer text color.', 'modern-audio-player' ), ), 'player_accent_color' => array( 'label' => __( 'Accent color', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Highlights the preset label and interactive emphasis states.', 'modern-audio-player' ), ), 'player_button_background_color' => array( 'label' => __( 'Button background', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Default play button fill color.', 'modern-audio-player' ), ), 'player_button_text_color' => array( 'label' => __( 'Button text', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Play button text and icon color.', 'modern-audio-player' ), ), 'player_progress_background_color' => array( 'label' => __( 'Progress background', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Unfilled progress track color.', 'modern-audio-player' ), ), 'player_progress_fill_color' => array( 'label' => __( 'Progress fill', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Filled portion of the progress track.', 'modern-audio-player' ), ), 'player_border_color' => array( 'label' => __( 'Border color', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Border used around the player shell and dividers.', 'modern-audio-player' ), ), 'player_shadow_color' => array( 'label' => __( 'Shadow color', 'modern-audio-player' ), 'type' => 'color', 'description' => __( 'Base color used for depth and glow effects.', 'modern-audio-player' ), ), ), ), 'text' => array( 'title' => __( 'Text', 'modern-audio-player' ), 'description' => __( 'Typography defaults for every player instance.', 'modern-audio-player' ), 'fields' => array( 'player_font_size' => array( 'label' => __( 'Base font size', 'modern-audio-player' ), 'type' => 'range', 'description' => __( 'Applied as the root font size inside the player.', 'modern-audio-player' ), 'min' => 12, 'max' => 22, 'step' => 1, 'unit' => 'px', ), 'player_show_theme_label' => array( 'label' => __( 'Show preset label', 'modern-audio-player' ), 'type' => 'checkbox', 'description' => __( 'Displays the active preset label above the track title.', 'modern-audio-player' ), ), ), ), 'layout' => array( 'title' => __( 'Layout', 'modern-audio-player' ), 'description' => __( 'Spacing and shape tokens for the global player layout.', 'modern-audio-player' ), 'fields' => array( 'player_border_radius' => array( 'label' => __( 'Border radius', 'modern-audio-player' ), 'type' => 'range', 'description' => __( 'Rounds the player card and cover image corners.', 'modern-audio-player' ), 'min' => 0, 'max' => 40, 'step' => 1, 'unit' => 'px', ), 'player_padding' => array( 'label' => __( 'Player padding', 'modern-audio-player' ), 'type' => 'range', 'description' => __( 'Controls the internal spacing of the player shell.', 'modern-audio-player' ), 'min' => 12, 'max' => 36, 'step' => 1, 'unit' => 'px', ), ), ), 'controls' => array( 'title' => __( 'Controls', 'modern-audio-player' ), 'description' => __( 'Display options for the shared player controls and media artwork.', 'modern-audio-player' ), 'fields' => array( 'player_show_cover_image' => array( 'label' => __( 'Show cover image', 'modern-audio-player' ), 'type' => 'checkbox', 'description' => __( 'Displays the cover image whenever the player has artwork.', 'modern-audio-player' ), ), ), ), ); } /** * Get preset tokens. * * @return array> */ public static function get_design_presets() { $presets = array( 'modern-dark' => array( 'label' => __( 'Modern Dark', 'modern-audio-player' ), 'description' => __( 'High-contrast dark layout with bold controls.', 'modern-audio-player' ), 'tokens' => array( 'player_background_color' => '#0f172a', 'player_surface_color' => '#111827', 'player_text_color' => '#f9fafb', 'player_muted_text_color' => '#94a3b8', 'player_accent_color' => '#7dd3fc', 'player_button_background_color' => '#7dd3fc', 'player_button_text_color' => '#0f172a', 'player_progress_background_color' => '#334155', 'player_progress_fill_color' => '#7dd3fc', 'player_border_color' => '#1f2937', 'player_shadow_color' => '#0f172a', 'player_border_radius' => 18, 'player_padding' => 16, 'player_font_size' => 16, 'player_show_cover_image' => 1, 'player_show_theme_label' => 1, ), ), 'glassmorphism' => array( 'label' => __( 'Glassmorphism', 'modern-audio-player' ), 'description' => __( 'Translucent layered card with a soft backdrop effect.', 'modern-audio-player' ), 'tokens' => array( 'player_background_color' => '#1e293b', 'player_surface_color' => '#334155', 'player_text_color' => '#f8fafc', 'player_muted_text_color' => '#cbd5e1', 'player_accent_color' => '#93c5fd', 'player_button_background_color' => '#e2e8f0', 'player_button_text_color' => '#0f172a', 'player_progress_background_color' => '#64748b', 'player_progress_fill_color' => '#93c5fd', 'player_border_color' => '#94a3b8', 'player_shadow_color' => '#0f172a', 'player_border_radius' => 24, 'player_padding' => 18, 'player_font_size' => 16, 'player_show_cover_image' => 1, 'player_show_theme_label' => 1, ), ), 'podcast-style' => array( 'label' => __( 'Podcast Style', 'modern-audio-player' ), 'description' => __( 'Editorial player treatment for spoken-word content.', 'modern-audio-player' ), 'tokens' => array( 'player_background_color' => '#201533', 'player_surface_color' => '#0f172a', 'player_text_color' => '#fef3c7', 'player_muted_text_color' => '#cbd5e1', 'player_accent_color' => '#fdba74', 'player_button_background_color' => '#f97316', 'player_button_text_color' => '#fff7ed', 'player_progress_background_color' => '#334155', 'player_progress_fill_color' => '#f97316', 'player_border_color' => '#1d4ed8', 'player_shadow_color' => '#172554', 'player_border_radius' => 20, 'player_padding' => 20, 'player_font_size' => 17, 'player_show_cover_image' => 1, 'player_show_theme_label' => 1, ), ), ); return apply_filters( 'map_theme_presets', $presets ); } /** * Get the default preset slug. * * @return string */ public static function get_default_preset_slug() { return 'modern-dark'; } /** * Resolve effective design settings for a preset with saved overrides. * * @param string $preset_slug Preset slug. * @param array $settings Optional settings array. * @return array */ public static function get_resolved_design_settings( $preset_slug = '', $settings = array() ) { if ( empty( $settings ) ) { $settings = self::get(); } $presets = self::get_design_presets(); $preset_slug = sanitize_key( $preset_slug ? $preset_slug : $settings['default_theme'] ); if ( ! isset( $presets[ $preset_slug ] ) ) { $preset_slug = self::get_default_preset_slug(); } $resolved = $presets[ $preset_slug ]['tokens']; foreach ( self::get_design_field_keys() as $field_key ) { if ( '' !== $settings[ $field_key ] && null !== $settings[ $field_key ] ) { $resolved[ $field_key ] = $settings[ $field_key ]; } } $resolved['preset_slug'] = $preset_slug; return $resolved; } /** * Build CSS variable declarations from resolved design settings. * * @param array $design_settings Resolved settings. * @return string */ public static function build_design_css_variables( $design_settings ) { $map = array( '--velora-bg' => sanitize_hex_color( $design_settings['player_background_color'] ), '--velora-surface' => sanitize_hex_color( $design_settings['player_surface_color'] ), '--velora-text' => sanitize_hex_color( $design_settings['player_text_color'] ), '--velora-muted' => sanitize_hex_color( $design_settings['player_muted_text_color'] ), '--velora-accent' => sanitize_hex_color( $design_settings['player_accent_color'] ), '--velora-button-bg' => sanitize_hex_color( $design_settings['player_button_background_color'] ), '--velora-button-text' => sanitize_hex_color( $design_settings['player_button_text_color'] ), '--velora-progress-bg' => sanitize_hex_color( $design_settings['player_progress_background_color'] ), '--velora-progress-fill' => sanitize_hex_color( $design_settings['player_progress_fill_color'] ), '--velora-border' => sanitize_hex_color( $design_settings['player_border_color'] ), '--velora-shadow' => sanitize_hex_color( $design_settings['player_shadow_color'] ), '--velora-radius' => absint( $design_settings['player_border_radius'] ) . 'px', '--velora-padding' => absint( $design_settings['player_padding'] ) . 'px', '--velora-font-size' => absint( $design_settings['player_font_size'] ) . 'px', ); $styles = array(); foreach ( $map as $name => $value ) { $styles[] = $name . ':' . $value; } return implode( ';', $styles ); } /** * Update settings with sanitization. * * @param array $input Raw input. * @return array */ public static function sanitize( $input ) { $defaults = self::defaults(); $presets = self::get_design_presets(); $themes = array_keys( $presets ); $output = $defaults; $output['default_theme'] = in_array( $input['default_theme'] ?? '', $themes, true ) ? $input['default_theme'] : $defaults['default_theme']; $output['enabled_themes'] = array_values( array_intersect( $themes, array_map( 'sanitize_key', (array) ( $input['enabled_themes'] ?? $defaults['enabled_themes'] ) ) ) ); if ( empty( $output['enabled_themes'] ) ) { $output['enabled_themes'] = $defaults['enabled_themes']; } if ( ! in_array( $output['default_theme'], $output['enabled_themes'], true ) ) { $output['default_theme'] = $output['enabled_themes'][0]; } $preset_tokens = $presets[ $output['default_theme'] ]['tokens']; foreach ( self::get_design_field_definitions() as $field_key => $field ) { $baseline = $preset_tokens[ $field_key ]; $sanitized = self::sanitize_field_value( $field_key, $field, $input, $baseline ); $output[ $field_key ] = self::values_match( $sanitized, $baseline, $field['type'] ) ? '' : $sanitized; } return $output; } /** * Install default settings if missing. * * @return void */ public static function maybe_install_defaults() { if ( false === get_option( MAP_OPTION_KEY, false ) ) { add_option( MAP_OPTION_KEY, self::defaults() ); } } /** * Get the flat list of design field definitions. * * @return array> */ public static function get_design_field_definitions() { $fields = array(); foreach ( self::get_design_schema() as $section ) { foreach ( $section['fields'] as $field_key => $field ) { if ( 0 === strpos( $field_key, 'player_' ) ) { $fields[ $field_key ] = $field; } } } return $fields; } /** * Get the list of design field keys. * * @return array */ public static function get_design_field_keys() { return array_keys( self::get_design_field_definitions() ); } /** * Get select-ready preset options. * * @return array */ private static function get_preset_options() { $options = array(); foreach ( self::get_design_presets() as $slug => $preset ) { $options[ $slug ] = $preset['label']; } return $options; } /** * Sanitize a field value. * * @param string $field_key Field key. * @param array $field Field config. * @param array $input Raw input. * @param string|int $baseline Baseline preset value. * @return string|int */ private static function sanitize_field_value( $field_key, $field, $input, $baseline ) { switch ( $field['type'] ) { case 'color': return self::sanitize_color( $input[ $field_key ] ?? '', (string) $baseline ); case 'checkbox': return empty( $input[ $field_key ] ) ? 0 : 1; case 'range': $min = isset( $field['min'] ) ? (int) $field['min'] : 0; $max = isset( $field['max'] ) ? (int) $field['max'] : 999; return max( $min, min( $max, absint( $input[ $field_key ] ?? 0 ) ) ); } return sanitize_text_field( (string) ( $input[ $field_key ] ?? '' ) ); } /** * Check whether two values match for override storage. * * @param string|int $value Value. * @param string|int $baseline Baseline value. * @param string $type Field type. * @return bool */ private static function values_match( $value, $baseline, $type ) { if ( 'color' === $type ) { return strtolower( (string) $value ) === strtolower( (string) $baseline ); } return (string) $value === (string) $baseline; } /** * Sanitize a hex color. * * @param string $color Input color. * @param string $fallback Fallback color. * @return string */ private static function sanitize_color( $color, $fallback ) { $sanitized = sanitize_hex_color( $color ); return $sanitized ? $sanitized : $fallback; } /** * Migrate legacy visual settings into the new design keys. * * @param array $settings Stored settings. * @return array */ private static function migrate_legacy_settings( $settings ) { $legacy_map = array( 'accent_color' => 'player_accent_color', 'surface_color' => 'player_surface_color', 'text_color' => 'player_text_color', 'border_radius' => 'player_border_radius', 'show_cover_image' => 'player_show_cover_image', ); foreach ( $legacy_map as $legacy_key => $new_key ) { if ( ! isset( $settings[ $new_key ] ) && isset( $settings[ $legacy_key ] ) ) { $settings[ $new_key ] = $settings[ $legacy_key ]; } } return $settings; } }