Station::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 1, 'fields' => 'ids', 'orderby' => 'menu_order title', 'order' => 'ASC', ) ); return $first ? (int) $first[0] : 0; } /** * Notice for editors only. * * @param string $message Message. * @return string */ public static function notice( $message ) { return current_user_can( 'edit_posts' ) ? '
' . esc_html( $message ) . '
' : ''; } /** * Enqueue the frontend bundle (widgets ship inside it). */ public static function assets() { Assets::enqueue(); } /** * Wrapper attributes from a block/shortcode context. * * @param string $class_name Base class. * @param array $atts Attributes (className / align honoured). * @return string */ public static function wrap_attrs( $class_name, $atts ) { $classes = array( $class_name ); if ( ! empty( $atts['className'] ) ) { $classes[] = $atts['className']; } if ( ! empty( $atts['class'] ) ) { $classes[] = $atts['class']; } if ( ! empty( $atts['align'] ) ) { $classes[] = 'align' . $atts['align']; } return 'class="' . esc_attr( implode( ' ', array_map( 'sanitize_html_class', preg_split( '/\s+/', implode( ' ', $classes ) ) ) ) ) . '"'; } /* -------------------------------------------------------- now playing */ /** * Now playing island: server-rendered skeleton hydrated by the frontend bundle. * * @param array $atts { station, show_artwork, show_listeners, show_station, compact } plus add-on keys. * @return string */ public static function now_playing( $atts = array() ) { $atts = wp_parse_args( $atts, array( 'station' => 0, 'show_artwork' => true, 'show_listeners' => false, 'show_station' => true, 'compact' => false, 'interval' => 15, ) ); $station_id = self::station_id( $atts['station'] ); $station = $station_id ? Station::to_public( $station_id ) : null; if ( ! $station ) { return self::notice( __( 'StreamSera: no published station to show.', 'streamsera' ) ); } self::assets(); $options = array( 'artwork' => (bool) $atts['show_artwork'], 'listeners' => (bool) $atts['show_listeners'], 'station' => (bool) $atts['show_station'], 'compact' => (bool) $atts['compact'], 'interval' => max( 5, min( 120, absint( $atts['interval'] ) ) ), ); $payload = array( 'station' => Station::without_contact( $station ), 'options' => $options, ); /** * Filter the now-playing widget payload (add-ons add options their widget extensions read). * * @param array $payload Payload { station, options }. * @param int $station_id Station. * @param array $atts Widget arguments (block / element / shortcode / classic widget). */ $payload = apply_filters( 'streamsera_now_playing_widget_payload', $payload, $station_id, $atts ); $classes = 'streamsera-widget streamsera-widget--now-playing' . ( $options['compact'] ? ' is-compact' : '' ); $html = sprintf( '
', self::wrap_attrs( $classes, $atts ), $station_id, esc_attr( (string) wp_json_encode( $payload ) ) ); if ( $options['artwork'] ) { $html .= '
' . ( $station['logo'] ? '' : '' ) . '
'; } $html .= '
' . esc_html__( 'Now playing', 'streamsera' ) . '' . esc_html( $station['name'] ) . ''; if ( $options['station'] && $station['slogan'] ) { $html .= '' . esc_html( $station['slogan'] ) . ''; } $html .= '
'; return $html; } /** * Shortcode: [streamsera_now_playing station="" artwork="1" listeners="0" compact="0"]. * Attributes add-ons understand are passed through to now_playing(). * * @param array $atts Attributes. * @return string */ public static function shortcode_now_playing( $atts ) { // Extra attributes are passed on to add-ons (sanitized as text); the known ones are validated below. $raw = is_array( $atts ) ? map_deep( $atts, 'sanitize_text_field' ) : array(); $atts = shortcode_atts( array( 'station' => 0, 'artwork' => '1', 'listeners' => '0', 'compact' => '0', 'class' => '', ), $atts, 'streamsera_now_playing' ); $html = self::now_playing( array_merge( $raw, array( 'station' => $atts['station'], 'show_artwork' => self::truthy( $atts['artwork'] ), 'show_listeners' => self::truthy( $atts['listeners'] ), 'compact' => self::truthy( $atts['compact'] ), 'class' => $atts['class'], ) ) ); return wp_kses( $html, streamsera_allowed_html() ); } /* ------------------------------------------------------ listen button */ /** * Where a listen button can lead (target → label). * * @return array */ public static function listen_targets() { /** * Filter the listen button targets (add-ons add e.g. a pop-out window or the sticky player). * * @param array $targets Target → label. */ return (array) apply_filters( 'streamsera_listen_button_targets', array( 'listen' => __( 'Listen page', 'streamsera' ) ) ); } /** * A button that opens the /listen page (or pop-out) in a new window, or toggles * the sticky player when one is on the page. * * @param array $atts { label, player, target, style, size }. * @return string */ public static function listen_button( $atts = array() ) { $atts = wp_parse_args( $atts, array( 'label' => __( 'Listen live', 'streamsera' ), 'player' => 0, 'target' => 'listen', // listen (+ add-on targets, see listen_targets()). 'style' => 'fill', // fill | outline | pill. 'size' => 'md', ) ); $player_id = absint( $atts['player'] ); if ( ! $player_id ) { $player_id = (int) streamsera_get_setting( 'listen_player', 0 ); } if ( ! $player_id ) { $first = get_posts( array( 'post_type' => \StreamSera\PostTypes\Player::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 1, 'fields' => 'ids', ) ); $player_id = $first ? (int) $first[0] : 0; } if ( ! $player_id ) { return self::notice( __( 'StreamSera: build a player first to use the listen button.', 'streamsera' ) ); } self::assets(); $target = array_key_exists( (string) $atts['target'], self::listen_targets() ) ? (string) $atts['target'] : 'listen'; if ( 'listen' === $target && ! streamsera_get_setting( 'listen_enabled', false ) ) { // No listen page: the button plays the first player on the page instead. $target = 'page'; } $url = 'listen' === $target ? \StreamSera\Modules\Listen_Page::url() : '#'; /** * Filter the listen button URL. * * @param string $url URL. * @param int $player_id Player. * @param string $target Target. */ $url = apply_filters( 'streamsera_listen_button_url', $url, $player_id, $target ); $variant = ! empty( $atts['variant'] ) ? $atts['variant'] : $atts['style']; $style = in_array( $variant, array( 'fill', 'outline', 'pill' ), true ) ? $variant : 'fill'; $size = in_array( $atts['size'], array( 'sm', 'md', 'lg' ), true ) ? $atts['size'] : 'md'; $class = 'streamsera-widget streamsera-widget--listen'; return sprintf( '
%8$s
', self::wrap_attrs( $class, $atts ), esc_attr( $style ), esc_attr( $size ), esc_url( $url ), $player_id, esc_attr( $target ), in_array( $target, array( 'listen', 'popout' ), true ) ? ' target="_blank" rel="noopener"' : '', esc_html( $atts['label'] ) ); } /** * Shortcode: [streamsera_listen_button label="Listen live" player="" target="listen" style="fill|outline|pill" size="sm|md|lg"]. * * @param array $atts Attributes. * @return string */ public static function shortcode_listen_button( $atts ) { $atts = shortcode_atts( array( 'label' => __( 'Listen live', 'streamsera' ), 'player' => 0, 'target' => 'listen', 'style' => 'fill', 'size' => 'md', 'class' => '', ), $atts, 'streamsera_listen_button' ); return wp_kses( self::listen_button( $atts ), streamsera_allowed_html() ); } /* ------------------------------------------------------------ station */ /** * Link to a station's public page (when station pages are on). * * @param int $station_id Station. * @return string */ public static function station_url( $station_id ) { if ( class_exists( '\\StreamSera\\Modules\\Station_Pages' ) && \StreamSera\Modules\Station_Pages::enabled() ) { return (string) \StreamSera\Modules\Station_Pages::url( $station_id ); } return ''; } /** * Social icon row. * * @param array $social network → url. * @return string */ public static function social_links( $social ) { $out = ''; foreach ( (array) $social as $network => $url ) { if ( ! $url || ! is_string( $url ) ) { continue; } $label = Icons::label( (string) $network ); $icon = Icons::svg( (string) $network ); $out .= '' . ( $icon ? $icon : Icons::svg( 'link' ) ) . ''; } return $out ? '

' . $out . '

' : ''; } /** * Individual genres from a station's genre text ("News, Talk" or "News · Talk"). * * @param string $genre Genre text. * @return string[] */ public static function genre_tokens( $genre ) { return array_values( array_filter( array_map( 'trim', preg_split( '/\s*[,·\/|;]\s*/u', (string) $genre ) ) ) ); } /** * Inner markup of one station card (shared by the station and station list elements). * * @param array $station Public station. * @param array $atts Display options. * @return string */ private static function station_card( $station, $atts ) { $url = self::station_url( (int) $station['id'] ); $details = array_filter( array( $station['genre'], $station['frequency'] ) ); $html = '
'; if ( ! empty( $atts['show_logo'] ) ) { $logo = $station['logo'] ? '' : ''; $html .= $url ? '' : ''; } $html .= '
'; $title = esc_html( $station['name'] ); $html .= '

' . ( $url ? '' . $title . '' : $title ) . '

'; if ( ! empty( $atts['show_details'] ) && $details ) { $html .= '

' . esc_html( implode( ' · ', $details ) ) . '

'; } if ( ! empty( $atts['show_slogan'] ) && $station['slogan'] ) { $html .= '

' . esc_html( $station['slogan'] ) . '

'; } if ( ! empty( $atts['show_description'] ) && $station['description'] ) { $html .= '

' . esc_html( wp_trim_words( $station['description'], 30 ) ) . '

'; } if ( ! empty( $atts['show_now_playing'] ) ) { $html .= self::now_playing( array( 'station' => (int) $station['id'], 'compact' => true, 'show_artwork' => false, 'show_station' => false, 'class' => 'ss-card__np', ) ); } if ( ! empty( $atts['show_social'] ) ) { $html .= self::social_links( $station['social'] ); } $cta = $atts['cta'] ?? 'page'; if ( 'listen' === $cta ) { $html .= '
' . self::listen_button( array( 'size' => 'sm', 'label' => __( 'Listen live', 'streamsera' ) ) ) . '
'; } elseif ( 'page' === $cta && $url ) { $html .= ''; } $html .= '
'; return $html; } /** * Default display options for station cards. * * @return array */ private static function station_defaults() { return array( 'show_logo' => true, 'show_details' => true, 'show_slogan' => true, 'show_description' => false, 'show_now_playing' => true, 'show_social' => false, 'cta' => 'page', // page | listen | none. 'style' => 'card', // card | horizontal. ); } /** * One station card. * * @param array $atts { station, show_logo, show_details, show_slogan, show_description, show_now_playing, show_social, cta, style }. * @return string */ public static function station( $atts = array() ) { $atts = wp_parse_args( $atts, array_merge( array( 'station' => 0 ), self::station_defaults() ) ); $station_id = self::station_id( $atts['station'] ); $station = $station_id ? Station::to_public( $station_id ) : null; if ( ! $station ) { return self::notice( __( 'StreamSera: no published station to show.', 'streamsera' ) ); } self::assets(); return '
' . self::station_card( $station, $atts ) . '
'; } /** * Station directory: cards for every published station, with optional search and genre filter. * * @param array $atts { count, columns, show_search, … station card options }. * @return string */ public static function stations( $atts = array() ) { $atts = wp_parse_args( $atts, array_merge( self::station_defaults(), array( 'count' => 0, 'columns' => 3, 'show_search' => true, 'show_now_playing' => false, 'orderby' => 'menu_order', // menu_order | title | date. ) ) ); $order = array( 'menu_order' => 'menu_order title', 'title' => 'title', 'date' => 'date', ); $ids = get_posts( array( 'post_type' => Station::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => absint( $atts['count'] ) ? absint( $atts['count'] ) : -1, // 0 = every published station. 'fields' => 'ids', 'orderby' => $order[ $atts['orderby'] ] ?? 'menu_order title', 'order' => 'date' === $atts['orderby'] ? 'DESC' : 'ASC', ) ); $items = array_values( array_filter( array_map( array( Station::class, 'to_public' ), $ids ) ) ); if ( ! $items ) { return self::notice( __( 'StreamSera: no published stations yet.', 'streamsera' ) ); } self::assets(); $cols = max( 1, min( 4, absint( $atts['columns'] ) ) ); $html = '
'; if ( $atts['show_search'] && count( $items ) > 1 ) { $genres = array(); foreach ( $items as $item ) { foreach ( self::genre_tokens( $item['genre'] ) as $token ) { $genres[ strtolower( $token ) ] = $token; } } natcasesort( $genres ); $html .= '
'; if ( count( $genres ) > 1 ) { $html .= '
'; foreach ( $genres as $key => $genre ) { $html .= ''; } $html .= '
'; } $html .= '
'; } $html .= '
'; foreach ( $items as $station ) { $html .= self::station_card( $station, $atts ); } $html .= '
'; return $html; } /* --------------------------------------------------- shortcode helpers */ /** * Map shortcode flags ("1"/"0") onto renderer booleans. * * @param array $atts Parsed shortcode attributes. * @param array $flags shortcode key → renderer key. * @return array */ public static function flags( $atts, $flags ) { $out = $atts; foreach ( $flags as $from => $to ) { if ( array_key_exists( $from, $atts ) ) { $out[ $to ] = self::truthy( $atts[ $from ] ); if ( $from !== $to ) { unset( $out[ $from ] ); } } } return $out; } /** * Shortcode: [streamsera_station station="" logo="1" details="1" slogan="1" description="0" nowplaying="1" social="0" cta="page|listen|none" style="card|horizontal"]. * * @param array $atts Attributes. * @return string */ public static function shortcode_station( $atts ) { $atts = shortcode_atts( array( 'station' => 0, 'logo' => '1', 'details' => '1', 'slogan' => '1', 'description' => '0', 'nowplaying' => '1', 'social' => '0', 'cta' => 'page', 'style' => 'card', 'class' => '' ), $atts, 'streamsera_station' ); return wp_kses( self::station( self::flags( $atts, array( 'logo' => 'show_logo', 'details' => 'show_details', 'slogan' => 'show_slogan', 'description' => 'show_description', 'nowplaying' => 'show_now_playing', 'social' => 'show_social' ) ) ), streamsera_allowed_html() ); } /** * Shortcode: [streamsera_stations count="" columns="3" search="1" logo="1" details="1" slogan="1" nowplaying="0" cta="page" orderby="menu_order|title|date"]. * * @param array $atts Attributes. * @return string */ public static function shortcode_stations( $atts ) { $atts = shortcode_atts( array( 'count' => 0, 'columns' => 3, 'search' => '1', 'logo' => '1', 'details' => '1', 'slogan' => '1', 'nowplaying' => '0', 'social' => '0', 'cta' => 'page', 'orderby' => 'menu_order', 'class' => '' ), $atts, 'streamsera_stations' ); return wp_kses( self::stations( self::flags( $atts, array( 'search' => 'show_search', 'logo' => 'show_logo', 'details' => 'show_details', 'slogan' => 'show_slogan', 'nowplaying' => 'show_now_playing', 'social' => 'show_social' ) ) ), streamsera_allowed_html() ); } /** * Shortcode boolean. * * @param mixed $v Value. * @return bool */ public static function truthy( $v ) { return filter_var( $v, FILTER_VALIDATE_BOOLEAN ); } }