radio_get_state(),
'stations' => radio_get_stations_flat(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'radio_save_state' ),
'popoutUrl' => admin_url( 'admin-post.php?action=radio_popout' ),
'strings' => array(
'play' => __( 'Play', 'rangerhq-radio' ),
'pause' => __( 'Pause', 'rangerhq-radio' ),
'loading' => __( 'Loading…', 'rangerhq-radio' ),
'error' => __( 'Stream error — try another station.', 'rangerhq-radio' ),
'saveError' => __( 'Preferences not saved — check your connection.', 'rangerhq-radio' ),
'mute' => __( 'Mute', 'rangerhq-radio' ),
'unmute' => __( 'Unmute', 'rangerhq-radio' ),
'nowPlaying' => __( 'Now Playing', 'rangerhq-radio' ),
'volume' => __( 'Volume', 'rangerhq-radio' ),
'station' => __( 'Station', 'rangerhq-radio' ),
'addFav' => __( 'Add to favourites', 'rangerhq-radio' ),
'removeFav' => __( 'Remove from favourites', 'rangerhq-radio' ),
'clearConfirm' => __( 'Clear all track history? (Favourites are kept.)', 'rangerhq-radio' ),
),
) );
}
/**
* AJAX endpoint for saving per-user state (station / volume changes).
* Avoids a full page reload on every interaction.
*/
add_action( 'wp_ajax_radio_save_state', 'radio_ajax_save_state' );
function radio_ajax_save_state() {
if ( ! is_user_logged_in() ) {
wp_send_json_error( 'Not logged in.', 401 );
}
check_ajax_referer( 'radio_save_state', 'nonce' );
$patch = array();
if ( isset( $_POST['station_id'] ) ) {
$patch['station_id'] = sanitize_key( wp_unslash( $_POST['station_id'] ) );
}
if ( isset( $_POST['volume'] ) ) {
$patch['volume'] = max( 0.0, min( 1.0, (float) $_POST['volume'] ) );
}
if ( empty( $patch ) ) {
wp_send_json_error( 'Nothing to save.', 400 );
}
radio_update_state( $patch );
wp_send_json_success( radio_get_state() );
}
/**
* AJAX: log a played track to the user's history (v0.5.0).
* Nonce: same `radio_save_state` token used for state saves — both come
* from the same `wp_localize_script` config available on player pages.
*/
add_action( 'wp_ajax_radio_log_track', 'radio_ajax_log_track' );
function radio_ajax_log_track() {
if ( ! is_user_logged_in() ) { wp_send_json_error( 'Not logged in.', 401 ); }
check_ajax_referer( 'radio_save_state', 'nonce' );
// Unslash + sanitize at access; radio_sanitize_entry() also re-sanitizes
// downstream as belt+braces, but PCP wants the cleanup AT the access point.
$logged = radio_log_track( array(
'artist' => isset( $_POST['artist'] ) ? sanitize_text_field( wp_unslash( $_POST['artist'] ) ) : '',
'title' => isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '',
'station' => isset( $_POST['station'] ) ? sanitize_text_field( wp_unslash( $_POST['station'] ) ) : '',
'station_id' => isset( $_POST['station_id'] ) ? sanitize_key( wp_unslash( $_POST['station_id'] ) ) : '',
) );
wp_send_json_success( array( 'logged' => (bool) $logged ) );
}
/**
* AJAX: toggle whether a track is favourited.
* Nonce: `radio_history` — created fresh per History-page render so the
* AJAX is gated to users who actually loaded the page.
*/
add_action( 'wp_ajax_radio_toggle_favourite', 'radio_ajax_toggle_favourite' );
function radio_ajax_toggle_favourite() {
if ( ! is_user_logged_in() ) { wp_send_json_error( 'Not logged in.', 401 ); }
check_ajax_referer( 'radio_history', 'nonce' );
$new_state = radio_toggle_favourite( array(
'artist' => isset( $_POST['artist'] ) ? sanitize_text_field( wp_unslash( $_POST['artist'] ) ) : '',
'title' => isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '',
'station' => isset( $_POST['station'] ) ? sanitize_text_field( wp_unslash( $_POST['station'] ) ) : '',
'station_id' => isset( $_POST['station_id'] ) ? sanitize_key( wp_unslash( $_POST['station_id'] ) ) : '',
) );
wp_send_json_success( array( 'favourite' => (bool) $new_state ) );
}
/** AJAX: clear the current user's history (favourites preserved). */
add_action( 'wp_ajax_radio_clear_history', 'radio_ajax_clear_history' );
function radio_ajax_clear_history() {
if ( ! is_user_logged_in() ) { wp_send_json_error( 'Not logged in.', 401 ); }
check_ajax_referer( 'radio_history', 'nonce' );
radio_clear_history_all();
wp_send_json_success();
}
/**
* v0.6.0 — Pop-out mini player. Renders a standalone HTML page (no WP
* admin chrome) at `admin-post.php?action=radio_popout`, opened by the
* JS via `window.open()`. The popup persists across main-tab navigation
* so background music keeps playing when the user moves around the
* admin. `&play=1` in the URL tells `radio.js` to auto-resume on load.
*/
add_action( 'admin_post_radio_popout', 'radio_render_popout_page' );
function radio_render_popout_page() {
if ( ! current_user_can( 'read' ) ) {
wp_die( esc_html__( 'You do not have permission to view this page.', 'rangerhq-radio' ) );
}
$state = radio_get_state();
$station = radio_find_station( $state['station_id'] );
$stations = radio_get_stations_grouped();
$theme = isset( $state['theme'] ) ? $state['theme'] : 'auto';
if ( ! in_array( $theme, array( 'auto', 'light', 'dark' ), true ) ) { $theme = 'auto'; }
$cfg = array(
'state' => $state,
'stations' => radio_get_stations_flat(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'radio_save_state' ),
'popoutUrl' => '', // already in popout — no further popouts
'autoPlay' => isset( $_GET['play'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- query flag only, no state change
'strings' => array(
'play' => __( 'Play', 'rangerhq-radio' ),
'pause' => __( 'Pause', 'rangerhq-radio' ),
'loading' => __( 'Loading…', 'rangerhq-radio' ),
'error' => __( 'Stream error — try another station.', 'rangerhq-radio' ),
'saveError' => __( 'Preferences not saved — check your connection.', 'rangerhq-radio' ),
'mute' => __( 'Mute', 'rangerhq-radio' ),
'unmute' => __( 'Unmute', 'rangerhq-radio' ),
'nowPlaying' => __( 'Now Playing', 'rangerhq-radio' ),
'volume' => __( 'Volume', 'rangerhq-radio' ),
'station' => __( 'Station', 'rangerhq-radio' ),
),
);
/*
* v0.7.0: Enqueue popup assets via WP so we can emit them with
* wp_print_styles() / wp_print_footer_scripts() — passes PCP's
* NonEnqueuedStylesheet / NonEnqueuedScript checks (we used raw
* /