349 lines
12 KiB
PHP
349 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Frontend renderer.
|
|
*
|
|
* @package ModernAudioPlayer
|
|
*/
|
|
|
|
namespace ModernAudioPlayer;
|
|
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
class Renderer {
|
|
/**
|
|
* Render a player instance.
|
|
*
|
|
* @param array<string, mixed> $attributes Player attributes.
|
|
* @param string $context Rendering context.
|
|
* @return string
|
|
*/
|
|
public static function render_player( $attributes, $context = 'frontend' ) {
|
|
self::enqueue_block_assets();
|
|
|
|
$settings = Settings::get();
|
|
$args = self::normalize_attributes( $attributes, $settings );
|
|
$theme_data = self::get_theme_presets();
|
|
$theme_name = isset( $theme_data[ $args['theme'] ] ) ? $theme_data[ $args['theme'] ]['label'] : $theme_data[ Settings::get_default_preset_slug() ]['label'];
|
|
$design_settings = Settings::get_resolved_design_settings( $args['theme'], $settings );
|
|
$style_attr = Settings::build_design_css_variables( $design_settings );
|
|
$is_editor_context = self::is_editor_context( $context );
|
|
$playlist_payload = self::prepare_playlist_payload( $args['playlist'] );
|
|
$current_track = $playlist_payload[ $args['currentIndex'] ];
|
|
$show_cover_image = ! empty( $current_track['image'] ) && ! empty( $design_settings['player_show_cover_image'] );
|
|
$instance_id = 'map-player-' . wp_unique_id();
|
|
|
|
if ( empty( $current_track['src'] ) ) {
|
|
if ( $is_editor_context ) {
|
|
return self::render_placeholder( $args['theme'], $theme_name, $style_attr, $design_settings );
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
ob_start();
|
|
?>
|
|
<div
|
|
id="<?php echo esc_attr( $instance_id ); ?>"
|
|
class="map-player map-theme-<?php echo esc_attr( $args['theme'] ); ?><?php echo $show_cover_image ? '' : ' map-player--no-cover'; ?>"
|
|
data-map-player
|
|
data-map-editor-preview="<?php echo $is_editor_context ? 'true' : 'false'; ?>"
|
|
data-map-show-cover-setting="<?php echo ! empty( $design_settings['player_show_cover_image'] ) ? 'true' : 'false'; ?>"
|
|
data-map-playlist="<?php echo esc_attr( wp_json_encode( $playlist_payload ) ); ?>"
|
|
data-map-current-index="<?php echo esc_attr( (string) $args['currentIndex'] ); ?>"
|
|
data-map-repeat-enabled="false"
|
|
data-map-has-cover="<?php echo $show_cover_image ? 'true' : 'false'; ?>"
|
|
data-track-src="<?php echo esc_url( $current_track['src'] ); ?>"
|
|
data-track-title="<?php echo esc_attr( $current_track['title'] ); ?>"
|
|
data-track-hash="<?php echo esc_attr( $current_track['hash'] ); ?>"
|
|
data-track-nonce="<?php echo esc_attr( $current_track['nonce'] ); ?>"
|
|
data-rest-endpoint="<?php echo esc_url( rest_url( 'map/v1/track-play' ) ); ?>"
|
|
style="<?php echo esc_attr( $style_attr ); ?>"
|
|
>
|
|
<div class="map-player__cover-wrap"<?php echo $show_cover_image ? '' : ' style="display:none;"'; ?>>
|
|
<img class="map-player__cover" src="<?php echo esc_url( $current_track['image'] ); ?>" alt="<?php echo esc_attr( $current_track['title'] ); ?>" loading="lazy" data-map-cover />
|
|
</div>
|
|
|
|
<div class="map-player__content">
|
|
<div class="map-player__header">
|
|
<?php if ( ! empty( $design_settings['player_show_theme_label'] ) ) : ?>
|
|
<div class="map-player__eyebrow"><?php echo esc_html( $theme_name ); ?></div>
|
|
<?php endif; ?>
|
|
<div class="map-player__title" data-map-title><?php echo esc_html( $current_track['title'] ); ?></div>
|
|
<?php if ( $is_editor_context ) : ?>
|
|
<p class="map-player__editor-note" data-map-editor-notice><?php esc_html_e( 'Playback preview is limited in the editor. Test full playback on the frontend.', 'modern-audio-player' ); ?></p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="map-player__controls">
|
|
<div class="map-player__actions">
|
|
<button type="button" class="map-player__toggle" data-map-toggle aria-label="<?php esc_attr_e( 'Play audio', 'modern-audio-player' ); ?>" aria-pressed="false">
|
|
<span class="map-player__toggle-icon map-player__toggle-icon--play" aria-hidden="true"></span>
|
|
<span class="map-player__toggle-text"><?php esc_html_e( 'Play', 'modern-audio-player' ); ?></span>
|
|
</button>
|
|
|
|
<div class="map-player__transport">
|
|
<button type="button" class="map-player__icon-button" data-map-previous aria-label="<?php esc_attr_e( 'Previous track', 'modern-audio-player' ); ?>" disabled>
|
|
<span class="map-player__icon map-player__icon--previous" aria-hidden="true"></span>
|
|
</button>
|
|
<button type="button" class="map-player__icon-button" data-map-next aria-label="<?php esc_attr_e( 'Next track', 'modern-audio-player' ); ?>">
|
|
<span class="map-player__icon map-player__icon--next" aria-hidden="true"></span>
|
|
</button>
|
|
<button type="button" class="map-player__icon-button" data-map-repeat aria-label="<?php esc_attr_e( 'Repeat track', 'modern-audio-player' ); ?>" aria-pressed="false">
|
|
<span class="map-player__icon map-player__icon--repeat" aria-hidden="true"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="map-player__timeline">
|
|
<input
|
|
type="range"
|
|
class="map-player__progress"
|
|
data-map-progress
|
|
min="0"
|
|
max="100"
|
|
value="0"
|
|
step="0.1"
|
|
aria-label="<?php esc_attr_e( 'Track progress', 'modern-audio-player' ); ?>"
|
|
/>
|
|
<div class="map-player__time">
|
|
<span data-map-current><?php esc_html_e( '0:00', 'modern-audio-player' ); ?></span>
|
|
<span data-map-duration><?php esc_html_e( '0:00', 'modern-audio-player' ); ?></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<audio class="map-player__audio" preload="metadata" src="<?php echo esc_url( $current_track['src'] ); ?>" data-map-audio></audio>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
return (string) ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Get available theme presets.
|
|
*
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
public static function get_theme_presets() {
|
|
return Settings::get_design_presets();
|
|
}
|
|
|
|
/**
|
|
* Normalize and sanitize player attributes.
|
|
*
|
|
* @param array<string, mixed> $attributes Raw attributes.
|
|
* @param array<string, mixed> $settings Global settings.
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function normalize_attributes( $attributes, $settings = array() ) {
|
|
if ( empty( $settings ) ) {
|
|
$settings = Settings::get();
|
|
}
|
|
|
|
$defaults = array(
|
|
'src' => '',
|
|
'title' => __( 'Untitled Track', 'modern-audio-player' ),
|
|
'image' => '',
|
|
'playlist' => array(),
|
|
'theme' => $settings['default_theme'],
|
|
'useGlobalTheme' => true,
|
|
);
|
|
|
|
$args = wp_parse_args( is_array( $attributes ) ? $attributes : array(), $defaults );
|
|
|
|
$args['src'] = esc_url_raw( $args['src'] );
|
|
$args['title'] = sanitize_text_field( $args['title'] );
|
|
$args['image'] = esc_url_raw( $args['image'] );
|
|
|
|
$use_global_theme = rest_sanitize_boolean( $args['useGlobalTheme'] );
|
|
$enabled_themes = isset( $settings['enabled_themes'] ) && is_array( $settings['enabled_themes'] ) ? $settings['enabled_themes'] : array_keys( self::get_theme_presets() );
|
|
$requested_theme = sanitize_key( $args['theme'] );
|
|
|
|
if ( $use_global_theme || ! in_array( $requested_theme, $enabled_themes, true ) ) {
|
|
$args['theme'] = $settings['default_theme'];
|
|
} else {
|
|
$args['theme'] = $requested_theme;
|
|
}
|
|
|
|
$args['playlist'] = self::normalize_playlist_items( $args['playlist'], $args );
|
|
|
|
if ( empty( $args['playlist'] ) ) {
|
|
$args['playlist'] = array(
|
|
self::prepare_track(
|
|
array(
|
|
'src' => $args['src'],
|
|
'title' => $args['title'],
|
|
'image' => $args['image'],
|
|
)
|
|
),
|
|
);
|
|
}
|
|
|
|
$args['currentIndex'] = 0;
|
|
$args['src'] = $args['playlist'][0]['src'];
|
|
$args['title'] = $args['playlist'][0]['title'];
|
|
$args['image'] = $args['playlist'][0]['image'];
|
|
$args['useGlobalTheme'] = $use_global_theme;
|
|
|
|
return $args;
|
|
}
|
|
|
|
/**
|
|
* Detect whether the current render is happening inside the block editor.
|
|
*
|
|
* @param string $context Rendering context.
|
|
* @return bool
|
|
*/
|
|
private static function is_editor_context( $context ) {
|
|
if ( 'editor' === $context || is_admin() ) {
|
|
return true;
|
|
}
|
|
|
|
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
|
|
return false;
|
|
}
|
|
|
|
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
|
|
$rest_context = isset( $_REQUEST['context'] ) ? sanitize_key( wp_unslash( $_REQUEST['context'] ) ) : '';
|
|
$referer = wp_get_referer();
|
|
|
|
if ( 'edit' === $rest_context || false !== strpos( $request_uri, '/wp/v2/block-renderer/' ) ) {
|
|
return true;
|
|
}
|
|
|
|
return is_string( $referer ) && false !== strpos( $referer, 'post.php' );
|
|
}
|
|
|
|
/**
|
|
* Render a visible placeholder for editor previews without an audio source.
|
|
*
|
|
* @param string $theme_slug Theme class suffix.
|
|
* @param string $theme_name Human-readable theme name.
|
|
* @param string $style_attr Inline CSS variable string.
|
|
* @param array<string, mixed> $design_settings Resolved design settings.
|
|
* @return string
|
|
*/
|
|
private static function render_placeholder( $theme_slug, $theme_name, $style_attr, $design_settings ) {
|
|
ob_start();
|
|
?>
|
|
<div
|
|
class="map-player map-player--placeholder map-theme-<?php echo esc_attr( $theme_slug ); ?>"
|
|
data-map-player
|
|
data-map-editor-preview="true"
|
|
style="<?php echo esc_attr( $style_attr ); ?>"
|
|
>
|
|
<div class="map-player__content">
|
|
<div class="map-player__header">
|
|
<?php if ( ! empty( $design_settings['player_show_theme_label'] ) ) : ?>
|
|
<div class="map-player__eyebrow"><?php echo esc_html( $theme_name ); ?></div>
|
|
<?php endif; ?>
|
|
<div class="map-player__title"><?php esc_html_e( 'Select an audio file to preview', 'modern-audio-player' ); ?></div>
|
|
<p class="map-player__editor-note" data-map-editor-notice><?php esc_html_e( 'Playback preview is limited in the editor. Test full playback on the frontend.', 'modern-audio-player' ); ?></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
return (string) ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Normalize playlist items and preserve backward compatibility.
|
|
*
|
|
* @param mixed $playlist Raw playlist attribute.
|
|
* @param array<string, mixed> $args Normalized player arguments.
|
|
* @return array<int, array<string, string>>
|
|
*/
|
|
private static function normalize_playlist_items( $playlist, $args ) {
|
|
$normalized = array();
|
|
|
|
if ( is_array( $playlist ) ) {
|
|
foreach ( $playlist as $track ) {
|
|
$prepared = self::prepare_track( is_array( $track ) ? $track : array() );
|
|
|
|
if ( '' !== $prepared['src'] ) {
|
|
$normalized[] = $prepared;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( empty( $normalized ) && ! empty( $args['src'] ) ) {
|
|
$normalized[] = self::prepare_track(
|
|
array(
|
|
'src' => $args['src'],
|
|
'title' => $args['title'],
|
|
'image' => $args['image'],
|
|
)
|
|
);
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
/**
|
|
* Sanitize a track item.
|
|
*
|
|
* @param array<string, mixed> $track Track data.
|
|
* @return array<string, string>
|
|
*/
|
|
private static function prepare_track( $track ) {
|
|
return array(
|
|
'src' => esc_url_raw( $track['src'] ?? '' ),
|
|
'title' => sanitize_text_field( $track['title'] ?? __( 'Untitled Track', 'modern-audio-player' ) ),
|
|
'image' => esc_url_raw( $track['image'] ?? '' ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Prepare playlist payload for the frontend runtime.
|
|
*
|
|
* @param array<int, array<string, string>> $playlist Playlist items.
|
|
* @return array<int, array<string, string>>
|
|
*/
|
|
private static function prepare_playlist_payload( $playlist ) {
|
|
$payload = array();
|
|
|
|
foreach ( $playlist as $track ) {
|
|
$hash = Analytics::build_source_hash( $track['src'] );
|
|
$payload[] = array(
|
|
'src' => $track['src'],
|
|
'title' => $track['title'],
|
|
'image' => $track['image'],
|
|
'hash' => $hash,
|
|
'nonce' => wp_create_nonce( 'map_track_play_' . $hash ),
|
|
);
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* Enqueue frontend block assets for shortcode and dynamic rendering.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function enqueue_block_assets() {
|
|
if ( is_admin() ) {
|
|
return;
|
|
}
|
|
|
|
$registry = \WP_Block_Type_Registry::get_instance();
|
|
$block = $registry->get_registered( 'velora/audio-player' );
|
|
|
|
if ( ! $block ) {
|
|
$block = $registry->get_registered( 'map/audio-player' );
|
|
}
|
|
|
|
if ( ! $block ) {
|
|
return;
|
|
}
|
|
|
|
foreach ( $block->style_handles as $handle ) {
|
|
wp_enqueue_style( $handle );
|
|
}
|
|
|
|
foreach ( $block->script_handles as $handle ) {
|
|
wp_enqueue_script( $handle );
|
|
}
|
|
}
|
|
}
|