'now_playing',
'listen-button' => 'listen_button',
);
/**
* Register.
*/
public function register() {
add_action( 'init', array( $this, 'register_blocks' ) );
// After the hand-made blocks: generate the rest from the element registry.
add_action( 'init', array( $this, 'register_element_blocks' ), 30 );
add_action( 'init', array( Widgets::class, 'shortcodes' ) );
add_action( 'init', array( $this, 'register_patterns' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'editor_data' ) );
add_action( 'save_post_' . \StreamSera\PostTypes\Player::POST_TYPE, array( $this, 'clear_first_id' ) );
add_action( 'before_delete_post', array( $this, 'clear_first_id' ) );
add_filter( 'block_categories_all', array( $this, 'category' ), 10 );
add_filter( 'register_block_type_args', array( $this, 'block_args' ), 10, 2 );
}
/**
* Put every streamsera/* block in the StreamSera category.
*
* @param array $args Args.
* @param string $name Block name.
* @return array
*/
public function block_args( $args, $name ) {
if ( 0 === strpos( (string) $name, 'streamsera/' ) ) {
$args['category'] = 'streamsera';
}
return $args;
}
/**
* Register the dynamic blocks.
*/
public function register_blocks() {
foreach ( self::BLOCKS as $name => $method ) {
$dir = STREAMSERA_PATH . 'build/blocks/' . $name;
if ( ! file_exists( $dir . '/block.json' ) ) {
continue;
}
register_block_type(
$dir,
array(
'render_callback' => static function ( $attributes ) use ( $method ) {
$attributes = is_array( $attributes ) ? $attributes : array();
$inner = call_user_func( array( Widgets::class, $method ), self::attributes( $attributes ) );
if ( '' === $inner ) {
return '';
}
return sprintf( '
%s
', get_block_wrapper_attributes(), wp_kses( $inner, streamsera_allowed_html() ) );
},
)
);
}
}
/**
* Editor data for the hand-made blocks (listen button targets add-ons may extend).
*/
public function editor_data() {
$handle = generate_block_asset_handle( 'streamsera/listen-button', 'editorScript' );
$targets = array();
foreach ( Widgets::listen_targets() as $value => $label ) {
$targets[] = array(
'value' => (string) $value,
'label' => html_entity_decode( (string) $label, ENT_QUOTES ),
);
}
if ( wp_script_is( $handle, 'registered' ) ) {
wp_add_inline_script( $handle, 'window.StreamSeraListenTargets = ' . wp_json_encode( $targets ) . ';', 'before' );
}
}
/**
* Generated blocks: every page element without a hand-made block becomes `streamsera/`,
* edited through one shared editor script that builds its controls from the field list.
*/
public function register_element_blocks() {
$registry = \WP_Block_Type_Registry::get_instance();
$asset = STREAMSERA_PATH . 'build/blocks/elements/index.asset.php';
if ( ! file_exists( $asset ) ) {
return;
}
$meta = require $asset;
wp_register_script( 'streamsera-element-blocks', STREAMSERA_URL . 'build/blocks/elements/index.js', $meta['dependencies'], $meta['version'], true );
wp_set_script_translations( 'streamsera-element-blocks', 'streamsera', STREAMSERA_PATH . 'languages' );
$editor = array();
foreach ( Elements::all() as $id => $el ) {
$name = $el['block'];
if ( ! $name || $registry->is_registered( $name ) ) {
continue;
}
$attributes = array();
$fields = array();
foreach ( $el['fields'] as $key => $field ) {
$attr = Elements::attr( $key );
$numeric = Elements::is_numeric_type( $field['type'] );
$attributes[ $attr ] = array(
'type' => 'toggle' === $field['type'] ? 'boolean' : ( $numeric ? 'number' : 'string' ),
// Numeric attributes need a numeric default ('' = "none chosen" becomes 0), or the editor preview request fails validation.
'default' => $numeric ? (int) $field['default'] : $field['default'],
);
$fields[] = array(
'key' => $key,
'attr' => $attr,
'type' => $field['type'],
'label' => $field['label'],
'help' => $field['help'],
'section' => $field['section'],
'min' => $field['min'] ?? null,
'max' => $field['max'] ?? null,
);
}
register_block_type(
$name,
array(
'api_version' => 3,
'title' => $el['label'],
'description' => $el['description'],
'category' => 'streamsera',
'icon' => $el['icon']['dashicon'],
'keywords' => $el['keywords'],
'attributes' => $attributes,
'supports' => array(
'html' => false,
'align' => array( 'wide', 'full' ),
'spacing' => array( 'margin' => true ),
),
'editor_script' => 'streamsera-element-blocks',
'render_callback' => static function ( $attrs ) use ( $id, $el ) {
$args = array();
foreach ( array_keys( $el['fields'] ) as $key ) {
$attr = Elements::attr( $key );
if ( array_key_exists( $attr, (array) $attrs ) ) {
$args[ $key ] = $attrs[ $attr ];
}
}
$inner = Elements::render( $id, $args );
return '' === $inner ? '' : sprintf( '%s
', get_block_wrapper_attributes(), wp_kses( $inner, streamsera_allowed_html() ) );
},
)
);
$editor[] = array(
'id' => $id,
'name' => $name,
'title' => $el['label'],
'description' => $el['description'],
'icon' => $el['icon']['dashicon'],
'keywords' => $el['keywords'],
'attributes' => $attributes,
'fields' => $fields,
);
}
if ( ! $editor ) {
return;
}
// Picker options (players, stations …) are only needed in the editor: query them there.
add_action(
'enqueue_block_editor_assets',
static function () use ( $editor ) {
foreach ( $editor as $i => $block ) {
$el = Elements::get( $block['id'] );
foreach ( $block['fields'] as $j => $field ) {
$options = array();
foreach ( Elements::options( $el['fields'][ $field['key'] ] ) as $value => $label ) {
$options[] = array(
'value' => (string) $value,
'label' => html_entity_decode( (string) $label, ENT_QUOTES ),
);
}
$editor[ $i ]['fields'][ $j ]['options'] = $options;
}
}
wp_add_inline_script( 'streamsera-element-blocks', 'window.StreamSeraElements = ' . wp_json_encode( array( 'blocks' => $editor, 'pickers' => array_keys( Elements::picker_types() ) ) ) . ';', 'before' );
}
);
}
/**
* Map block attributes (camelCase) to renderer args (snake_case).
*
* @param array $attributes Block attributes.
* @return array
*/
private static function attributes( $attributes ) {
$out = array();
// Alignment and extra classes belong to the block wrapper (get_block_wrapper_attributes()), not the inner markup.
unset( $attributes['align'], $attributes['className'] );
foreach ( $attributes as $key => $value ) {
$out[ strtolower( preg_replace( '/([a-z])([A-Z])/', '$1_$2', $key ) ) ] = $value;
}
return $out;
}
/**
* Add an "StreamSera" block category (used by every StreamSera block).
*
* @param array $categories Categories.
* @return array
*/
public function category( $categories ) {
foreach ( $categories as $cat ) {
if ( 'streamsera' === $cat['slug'] ) {
return $categories;
}
}
array_unshift(
$categories,
array(
'slug' => 'streamsera',
'title' => __( 'StreamSera', 'streamsera' ),
'icon' => null,
)
);
return $categories;
}
/**
* Block patterns for full-site editing themes.
*/
public function register_patterns() {
if ( ! function_exists( 'register_block_pattern_category' ) ) {
return;
}
register_block_pattern_category( 'streamsera', array( 'label' => __( 'StreamSera', 'streamsera' ) ) );
$player = self::first_id( \StreamSera\PostTypes\Player::POST_TYPE );
$name = get_bloginfo( 'name' );
$patterns = array(
'streamsera/hero-player' => array(
'title' => __( 'Hero player section', 'streamsera' ),
'description' => __( 'Full-width cover section with a headline, the live player and a listen button.', 'streamsera' ),
'content' => sprintf(
'
',
esc_html( $name ),
esc_html__( 'Live, 24 hours a day. Press play and stay with us.', 'streamsera' ),
$player,
// Block attributes are JSON: let core encode the translated label.
serialize_block_attributes(
array(
'label' => __( 'Open the listen page', 'streamsera' ),
'variant' => 'pill',
'size' => 'lg',
'align' => 'center',
)
)
),
),
'streamsera/sidebar-now-playing' => array(
'title' => __( 'Sidebar: now playing', 'streamsera' ),
'description' => __( 'Compact now-playing card with a listen button. Fits a sidebar or footer column.', 'streamsera' ),
'content' => '
' . esc_html__( 'On air', 'streamsera' ) . '
',
),
'streamsera/station-landing' => array(
'title' => __( 'Station landing page', 'streamsera' ),
'description' => __( 'A station home page: the player, what is playing now and your stations.', 'streamsera' ),
'content' => sprintf(
'
%2$s
%3$s
',
$player,
esc_html__( 'Now playing', 'streamsera' ),
esc_html__( 'Our stations', 'streamsera' )
),
),
);
/**
* Filter the block patterns StreamSera registers.
*
* @param array $patterns name → { title, description, content }.
*/
$patterns = apply_filters( 'streamsera_block_patterns', $patterns );
foreach ( $patterns as $slug => $pattern ) {
register_block_pattern(
$slug,
array_merge(
array(
'categories' => array( 'streamsera' ),
'keywords' => array( 'radio', 'streamsera', 'player' ),
),
$pattern
)
);
}
}
/**
* First published post id of a type (0 if none).
*
* @param string $type Post type.
* @return int
*/
public static function first_id( $type ) {
// Cached (patterns register on every request); cleared when a post of that type is saved or deleted.
$key = 'streamsera_first_' . $type;
$cached = get_transient( $key );
if ( false !== $cached ) {
return (int) $cached;
}
$ids = get_posts(
array(
'post_type' => $type,
'post_status' => 'publish',
'posts_per_page' => 1,
'fields' => 'ids',
'no_found_rows' => true,
'suppress_filters' => false,
)
);
$id = $ids ? (int) $ids[0] : 0;
set_transient( $key, $id, DAY_IN_SECONDS );
return $id;
}
/**
* Forget the cached first player after a player is saved or deleted.
*
* @param int $post_id Post ID.
*/
public function clear_first_id( $post_id ) {
$type = get_post_type( $post_id );
if ( \StreamSera\PostTypes\Player::POST_TYPE === $type ) {
delete_transient( 'streamsera_first_' . $type );
}
}
}