velora-player/includes/class-settings.php
2026-03-23 16:55:03 +01:00

517 lines
18 KiB
PHP

<?php
/**
* Settings service.
*
* @package ModernAudioPlayer
*/
namespace ModernAudioPlayer;
defined( 'ABSPATH' ) || exit;
class Settings {
/**
* Get default option values.
*
* @return array<string, mixed>
*/
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<string, mixed>
*/
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<string, array<string, mixed>>
*/
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<string, array<string, mixed>>
*/
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<string, mixed> $settings Optional settings array.
* @return array<string, mixed>
*/
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<string, mixed> $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<string, mixed> $input Raw input.
* @return array<string, mixed>
*/
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<string, array<string, mixed>>
*/
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<int, string>
*/
public static function get_design_field_keys() {
return array_keys( self::get_design_field_definitions() );
}
/**
* Get select-ready preset options.
*
* @return array<string, string>
*/
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<string, mixed> $field Field config.
* @param array<string, mixed> $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<string, mixed> $settings Stored settings.
* @return array<string, mixed>
*/
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;
}
}