prefix . 'aicmp_stats'; } // ── PROMO FEEDBACK STORAGE ── // Unlike the stats table, this one holds what people typed about themselves. // It is only ever written by the Skin 3 promo player, and only ever read in the // admin. Nothing is collected unless a promo player is actually used. function aicmp_feedback_table() { global $wpdb; return $wpdb->prefix . 'aicmp_feedback'; } function aicmp_install_stats() { global $wpdb; $table = aicmp_stats_table(); $charset = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, track_id BIGINT UNSIGNED NOT NULL, stat_date DATE NOT NULL, plays INT UNSIGNED NOT NULL DEFAULT 0, completes INT UNSIGNED NOT NULL DEFAULT 0, downloads INT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY track_date (track_id, stat_date) ) $charset;"; $fb_table = aicmp_feedback_table(); $sql2 = "CREATE TABLE $fb_table ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, playlist_id BIGINT UNSIGNED NOT NULL, fav_track BIGINT UNSIGNED NOT NULL DEFAULT 0, rating TINYINT UNSIGNED NOT NULL DEFAULT 0, comments TEXT NULL, name VARCHAR(120) NULL, country VARCHAR(80) NULL, organisation VARCHAR(160) NULL, website VARCHAR(255) NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY playlist (playlist_id), KEY created (created_at) ) $charset;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); dbDelta($sql2); update_option('aicmp_db_version', AICMP_DB_VERSION); add_option('aicmp_installed_at', time()); } register_activation_hook(__FILE__, 'aicmp_install_stats'); // Create/upgrade the table for sites that update the plugin without re-activating. add_action('plugins_loaded', function () { if (get_option('aicmp_db_version') !== AICMP_DB_VERSION) { aicmp_install_stats(); } }); // Public REST endpoint that increments a daily counter. Stores only totals. add_action('rest_api_init', function () { register_rest_route('aicmp/v1', '/stat', [ 'methods' => 'POST', 'permission_callback' => '__return_true', 'args' => [ 'track_id' => ['required' => true, 'sanitize_callback' => 'absint'], 'type' => ['required' => true, 'sanitize_callback' => 'sanitize_key'], ], 'callback' => function ($req) { $track_id = absint($req->get_param('track_id')); $type = sanitize_key($req->get_param('type')); $columns = ['play' => 'plays', 'complete' => 'completes', 'download' => 'downloads']; if (!$track_id || !isset($columns[$type]) || get_post_type($track_id) !== 'aicmp_track') { return new WP_REST_Response(['ok' => false], 400); } global $wpdb; $table = aicmp_stats_table(); $col = $columns[$type]; $date = current_time('Y-m-d'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- counter write on a custom table; must not be cached $wpdb->query($wpdb->prepare( "INSERT INTO %i (track_id, stat_date, %i) VALUES (%d, %s, 1) ON DUPLICATE KEY UPDATE %i = %i + 1", $table, $col, $track_id, $date, $col, $col )); return new WP_REST_Response(['ok' => true], 200); }, ]); }); function aicmp_stats_totals() { global $wpdb; $table = aicmp_stats_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- admin-only aggregate read on a custom table $row = $wpdb->get_row($wpdb->prepare("SELECT SUM(plays) p, SUM(completes) c, SUM(downloads) d FROM %i", $table), ARRAY_A); return [ 'plays' => (int) ($row['p'] ?? 0), 'completes' => (int) ($row['c'] ?? 0), 'downloads' => (int) ($row['d'] ?? 0), ]; } function aicmp_stats_by_track($limit = 100) { global $wpdb; $table = aicmp_stats_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- admin-only aggregate read on a custom table $rows = $wpdb->get_results($wpdb->prepare( "SELECT track_id, SUM(plays) plays, SUM(completes) completes, SUM(downloads) downloads FROM %i GROUP BY track_id ORDER BY plays DESC LIMIT %d", $table, $limit ), ARRAY_A); return $rows ?: []; } function aicmp_stats_range() { global $wpdb; $table = aicmp_stats_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- admin-only read on a custom table $row = $wpdb->get_row($wpdb->prepare("SELECT MIN(stat_date) mn, MAX(stat_date) mx FROM %i", $table), ARRAY_A); return ['from' => $row['mn'] ?? '', 'to' => $row['mx'] ?? '']; } function aicmp_stats_reset() { global $wpdb; $table = aicmp_stats_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- clears all stats on explicit admin request $wpdb->query($wpdb->prepare("TRUNCATE TABLE %i", $table)); } function aicmp_check_cap() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have sufficient permissions.', 'aicreativa-music-player')); } } // Allow lossless audio uploads (mp3, m4a, ogg, wav are already allowed by WordPress core). add_filter('upload_mimes', function ($mimes) { $mimes['flac'] = 'audio/flac'; $mimes['aif|aiff'] = 'audio/aiff'; return $mimes; }); // Derive a download extension from an audio URL (falls back to mp3). function aicmp_audio_ext($url) { $ext = strtolower(pathinfo(wp_parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION)); return $ext ? $ext : 'mp3'; } // ── POST TYPES ── add_action('init', function () { register_post_type('aicmp_track', [ 'labels' => ['name'=>'Tracks','singular_name'=>'Track','add_new_item'=>'Add New Track','edit_item'=>'Edit Track'], 'public' => false, 'show_ui' => true, 'show_in_menu' => false, 'supports' => ['title'], ]); register_post_type('aicmp_playlist', [ 'labels' => ['name'=>'Playlists','singular_name'=>'Playlist','add_new_item'=>'Add New Playlist','edit_item'=>'Edit Playlist'], 'public' => false, 'show_ui' => true, 'show_in_menu' => false, 'supports' => ['title'], ]); }); // ── ADMIN MENU ── add_action('admin_menu', function () { add_menu_page('AICreativa Music Player','AICreativa Music Player','manage_options','aicreativa-music-player','aicmp_dashboard_page','dashicons-format-audio',30); add_submenu_page('aicreativa-music-player','Dashboard','Dashboard','manage_options','aicreativa-music-player','aicmp_dashboard_page'); add_submenu_page('aicreativa-music-player','All Tracks','All Tracks','manage_options','edit.php?post_type=aicmp_track'); add_submenu_page('aicreativa-music-player','Add Track','Add Track','manage_options','post-new.php?post_type=aicmp_track'); add_submenu_page('aicreativa-music-player','All Playlists','All Playlists','manage_options','edit.php?post_type=aicmp_playlist'); add_submenu_page('aicreativa-music-player','Add Playlist','Add Playlist','manage_options','post-new.php?post_type=aicmp_playlist'); add_submenu_page('aicreativa-music-player','Settings','Settings','manage_options','aicmp-settings','aicmp_settings_page'); }); // ── ENQUEUE ADMIN SCRIPTS & STYLES ── add_action('admin_enqueue_scripts', function($hook) { $screen = get_current_screen(); // Only on track/playlist edit screens if (!$screen || !in_array($screen->post_type, ['aicmp_track', 'aicmp_playlist'])) return; // Enable WordPress Media Library wp_enqueue_media(); // Admin CSS wp_add_inline_style('wp-admin', ' .aicmp-field{margin-bottom:20px} .aicmp-field label{display:block;font-weight:600;margin-bottom:6px} .aicmp-field input[type=text],.aicmp-field input[type=url]{width:100%;padding:6px 10px;border:1px solid #8c8f94;border-radius:4px} .aicmp-artwork-preview{display:flex;align-items:center;gap:12px;margin-top:8px} .aicmp-artwork-preview img{width:80px;height:80px;object-fit:cover;border-radius:4px;border:1px solid #ddd} .aicmp-artwork-ph{width:80px;height:80px;background:#f0f0f0;border-radius:4px;border:1px dashed #ccc;display:flex;align-items:center;justify-content:center;font-size:28px} '); // Playlist builder CSS wp_add_inline_style('wp-admin', ' .aicmp-plb{display:flex;gap:16px;flex-wrap:wrap} .aicmp-plb-col{flex:1;min-width:260px} .aicmp-plb-col h4{margin:0 0 8px;font-size:13px} .aicmp-plb-list{border:1px solid #ddd;border-radius:4px;max-height:320px;overflow-y:auto;background:#fff;min-height:60px} .aicmp-plb-row{display:flex;align-items:center;gap:8px;padding:7px 10px;border-bottom:1px solid #f0f0f0;font-size:13px;background:#fff} .aicmp-plb-row:last-child{border-bottom:none} #aicmp_plb_sel .aicmp-plb-row{cursor:grab} #aicmp_plb_sel .aicmp-plb-row.dragging{opacity:.4} #aicmp_plb_sel .aicmp-plb-row.drag-over{border-top:2px solid #2271b1} .aicmp-plb-num{color:#999;width:18px;text-align:right;flex-shrink:0;font-size:12px} .aicmp-plb-grip{color:#bbb;cursor:grab;flex-shrink:0} .aicmp-plb-t{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .aicmp-plb-add,.aicmp-plb-rm{border:none;background:none;cursor:pointer;font-size:15px;line-height:1;padding:2px 6px;border-radius:3px;flex-shrink:0} .aicmp-plb-add{color:#2271b1}.aicmp-plb-add:hover{background:#f0f6fc} .aicmp-plb-rm{color:#b32d2e}.aicmp-plb-rm:hover{background:#fcf0f1} .aicmp-plb-empty{padding:14px;color:#999;font-size:12px;text-align:center} '); // Admin JS — media upload handlers wp_register_script('aicmp-admin', false, ['jquery', 'media-upload'], AICMP_VERSION, true); wp_enqueue_script('aicmp-admin'); wp_add_inline_script('aicmp-admin', ' jQuery(function($){ var mp3Frame, artFrame, pressFrame; $(document).on("click","#aicmp_promo_file_btn",function(e){ e.preventDefault(); if(pressFrame){pressFrame.open();return;} pressFrame=wp.media({ title:"' . esc_js(__('Select press material', 'aicreativa-music-player')) . '", button:{text:"' . esc_js(__('Use this file', 'aicreativa-music-player')) . '"}, multiple:false }); pressFrame.on("select",function(){ var att=pressFrame.state().get("selection").first().toJSON(); $("#aicmp_promo_file").val(att.url); $("#aicmp_promo_file_rm").show(); }); pressFrame.open(); }); $(document).on("click","#aicmp_promo_file_rm",function(e){ e.preventDefault(); $("#aicmp_promo_file").val(""); $(this).hide(); }); // MP3 Upload $(document).on("click","#aicmp_mp3_btn",function(e){ e.preventDefault(); if(mp3Frame){mp3Frame.open();return;} mp3Frame=wp.media({ title:"' . esc_js(__('Select audio file', 'aicreativa-music-player')) . '", button:{text:"' . esc_js(__('Use this file', 'aicreativa-music-player')) . '"}, library:{type:"audio"}, multiple:false }); mp3Frame.on("select",function(){ var att=mp3Frame.state().get("selection").first().toJSON(); $("#aicmp_mp3_url").val(att.url); }); mp3Frame.open(); }); // Artwork Upload $(document).on("click","#aicmp_artwork_btn",function(e){ e.preventDefault(); if(artFrame){artFrame.open();return;} artFrame=wp.media({ title:"' . esc_js(__('Select Artwork', 'aicreativa-music-player')) . '", button:{text:"' . esc_js(__('Use this image', 'aicreativa-music-player')) . '"}, multiple:false }); artFrame.on("select",function(){ var att=artFrame.state().get("selection").first().toJSON(); $("#aicmp_artwork_id").val(att.id); var prev=$("#aicmp_artwork_preview"); if(prev.is("img")){prev.attr("src",att.url);} else{prev.replaceWith("");} $("#aicmp_artwork_remove").show(); }); artFrame.open(); }); // Artwork Remove $(document).on("click","#aicmp_artwork_remove",function(){ $("#aicmp_artwork_id").val(""); $("#aicmp_artwork_preview").replaceWith("
🎵
"); $(this).hide(); }); }); '); // Playlist builder JS (drag & drop ordering) wp_add_inline_script('aicmp-admin', <<<'AICMPJS' (function(){ var avail=document.getElementById('aicmp_plb_avail'); var sel=document.getElementById('aicmp_plb_sel'); if(!avail||!sel)return; var emptyMsg=sel.querySelector('.aicmp-plb-empty'); var dragEl=null; function renumber(){ var rows=sel.querySelectorAll('.aicmp-plb-row'); rows.forEach(function(r,i){var n=r.querySelector('.aicmp-plb-num');if(n)n.textContent=(i+1)+'.';}); emptyMsg.style.display=rows.length?'none':''; } function makeSelRow(id,title){ var row=document.createElement('div'); row.className='aicmp-plb-row';row.draggable=true;row.dataset.id=id; row.innerHTML='⠇'; row.querySelector('.aicmp-plb-t').textContent=title; row.querySelector('input').value=id; bindDrag(row); return row; } function bindDrag(row){ row.addEventListener('dragstart',function(e){dragEl=row;row.classList.add('dragging');e.dataTransfer.effectAllowed='move';}); row.addEventListener('dragend',function(){row.classList.remove('dragging');sel.querySelectorAll('.aicmp-plb-row').forEach(function(x){x.classList.remove('drag-over');});renumber();}); row.addEventListener('dragover',function(e){e.preventDefault();sel.querySelectorAll('.aicmp-plb-row').forEach(function(x){x.classList.remove('drag-over');});if(row!==dragEl)row.classList.add('drag-over');}); row.addEventListener('drop',function(e){ e.preventDefault(); if(!dragEl||dragEl===row)return; var rows=Array.prototype.slice.call(sel.querySelectorAll('.aicmp-plb-row')); if(rows.indexOf(dragEl)post_type !== 'aicmp_playlist' || $post->post_status !== 'publish') { return 0; } return $id; } function aicmp_dock_was_rendered($set = null) { static $done = false; if ($set === true) { $done = true; } return $done; } add_action('wp_footer', 'aicmp_render_sitewide_dock', 5); function aicmp_render_sitewide_dock() { if (is_singular() && aicmp_dock_was_rendered()) { return; } $id = aicmp_sitewide_dock_id(); if (!$id) { return; } echo do_shortcode('[aicmp_playlist id="' . esc_attr($id) . '" skin="2"]'); } // ── CONTINUOUS PLAYBACK ── // Audio cannot survive a page load, and rewriting the page around the player // turned out to fight every theme differently. So instead of preventing the // reload, the dock remembers what it was playing and picks it back up. // Nothing about the page is touched, so nothing about the page can break. function aicmp_resume_js() { return <<<'AICMPRESUME' window.aicmpResume = { key: 'aicmp_resume_v1', save: function(state){ try{ sessionStorage.setItem(this.key, JSON.stringify(state)); }catch(e){} }, read: function(){ try{ var raw = sessionStorage.getItem(this.key); return raw ? JSON.parse(raw) : null; }catch(e){ return null; } }, clear: function(){ try{ sessionStorage.removeItem(this.key); }catch(e){} } }; AICMPRESUME; } // A short promo strip on the dashboard, so feedback is visible without having to // remember the screen exists. Stays out of the way until there is something in it. function aicmp_promo_summary() { global $wpdb; $total = aicmp_feedback_count(); if (!$total) { return; } $table = aicmp_feedback_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom plugin table $avg = $wpdb->get_var($wpdb->prepare("SELECT AVG(rating) FROM %i WHERE rating > 0", $table)); $recent = aicmp_feedback_rows(3); ?>

' . esc_html(number_format_i18n($total)) . '', '' . esc_html($avg ? number_format_i18n((float) $avg, 1) : '—') . '' ); ?>

post_title : '#' . $r['playlist_id'], $r['name'], $r['country'], $r['organisation'], $r['website'], $fav ? $fav->post_title : '', $r['rating'] ? $r['rating'] : '', $r['comments'], ]); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- php://output stream fclose($out); exit; } function aicmp_feedback_page() { aicmp_check_cap(); if (isset($_POST['aicmp_reset_feedback'])) { check_admin_referer('aicmp_reset_feedback'); $typed = strtoupper(trim(sanitize_text_field(wp_unslash($_POST['aicmp_reset_confirm'] ?? '')))); if ($typed === 'DELETE') { aicmp_feedback_reset(); echo '

' . esc_html__('All promo feedback has been deleted.', 'aicreativa-music-player') . '

'; } else { echo '

' . esc_html__('Nothing was deleted. Type DELETE in the box to confirm.', 'aicreativa-music-player') . '

'; } } $rows = aicmp_feedback_rows(); $total = aicmp_feedback_count(); ?>




post_title : '#' . $r['playlist_id']); ?> post_title : '—'); ?>

__('This page has been open a while. Please reload and try again.', 'aicreativa-music-player')], 403); } $playlist = absint(wp_unslash($_POST['playlist'] ?? 0)); $post = $playlist ? get_post($playlist) : null; if (!$post || $post->post_type !== 'aicmp_playlist' || $post->post_status !== 'publish') { wp_send_json_error(['message' => __('Unknown release.', 'aicreativa-music-player')], 400); } $comments = trim(sanitize_textarea_field(wp_unslash($_POST['comments'] ?? ''))); $rating = min(5, absint(wp_unslash($_POST['rating'] ?? 0))); $name = trim(sanitize_text_field(wp_unslash($_POST['name'] ?? ''))); // A rating or a comment is the whole point; a name says who it came from. if ($rating < 1 && $comments === '') { wp_send_json_error(['message' => __('Please leave a rating or a comment.', 'aicreativa-music-player')], 400); } if ($name === '') { wp_send_json_error(['message' => __('Please fill in your name under Your details.', 'aicreativa-music-player')], 400); } $website = isset($_POST['website']) ? esc_url_raw(wp_unslash($_POST['website'])) : ''; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- writing to the plugin's own table; no core API covers it $ok = $wpdb->insert(aicmp_feedback_table(), [ 'playlist_id' => $playlist, 'fav_track' => absint(wp_unslash($_POST['fav'] ?? 0)), 'rating' => $rating, 'comments' => mb_substr($comments, 0, 4000), 'name' => mb_substr($name, 0, 120), 'country' => mb_substr(trim(sanitize_text_field(wp_unslash($_POST['country'] ?? ''))), 0, 80), 'organisation' => mb_substr(trim(sanitize_text_field(wp_unslash($_POST['organisation'] ?? ''))), 0, 160), 'website' => $website, 'created_at' => current_time('mysql'), ], ['%d', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s']); if (!$ok) { wp_send_json_error(['message' => __('Could not save your feedback. Please try again.', 'aicreativa-music-player')], 500); } aicmp_notify_feedback($playlist, $name, $rating, $comments, [ 'country' => trim(sanitize_text_field(wp_unslash($_POST['country'] ?? ''))), 'organisation' => trim(sanitize_text_field(wp_unslash($_POST['organisation'] ?? ''))), 'website' => $website, 'fav' => absint(wp_unslash($_POST['fav'] ?? 0)), ]); wp_send_json_success(['message' => __('Thank you. Your downloads are now available.', 'aicreativa-music-player')]); } // Promo feedback is time-sensitive: you want to know while the record is still // going out, not the next time you happen to open the admin. function aicmp_notify_feedback($playlist_id, $name, $rating, $comments, $extra) { $to = trim((string) get_option('aicmp_feedback_email', '')); if ($to === '') { return; } if (!is_email($to)) { return; } $rel = get_post($playlist_id); $fav = $extra['fav'] ? get_post($extra['fav']) : null; /* translators: %s: release title */ $subject = sprintf(__('Promo feedback: %s', 'aicreativa-music-player'), $rel ? $rel->post_title : '#' . $playlist_id); $lines = []; /* translators: %s: name of the person who sent the feedback */ $lines[] = sprintf(__('From: %s', 'aicreativa-music-player'), $name); if ($extra['organisation']) { /* translators: %s: label, radio station or club the person is from */ $lines[] = sprintf(__('Organisation: %s', 'aicreativa-music-player'), $extra['organisation']); } if ($extra['country']) { /* translators: %s: country the person is in */ $lines[] = sprintf(__('Country: %s', 'aicreativa-music-player'), $extra['country']); } if ($extra['website']) { /* translators: %s: the person's website address */ $lines[] = sprintf(__('Website: %s', 'aicreativa-music-player'), $extra['website']); } $lines[] = ''; /* translators: %s: title of the release the feedback is about */ $lines[] = sprintf(__('Release: %s', 'aicreativa-music-player'), $rel ? $rel->post_title : '#' . $playlist_id); if ($fav) { /* translators: %s: title of the track they picked as their favourite */ $lines[] = sprintf(__('Favourite track: %s', 'aicreativa-music-player'), $fav->post_title); } if ($rating) { /* translators: %d: rating from 1 to 5 */ $lines[] = sprintf(__('Rating: %d of 5', 'aicreativa-music-player'), $rating); } if ($comments !== '') { $lines[] = ''; $lines[] = __('Comments:', 'aicreativa-music-player'); $lines[] = $comments; } $lines[] = ''; $lines[] = admin_url('admin.php?page=aicmp-feedback'); wp_mail($to, $subject, implode("\n", $lines)); } // ── PROMO FEEDBACK: READING ── function aicmp_feedback_rows($limit = 300) { global $wpdb; $table = aicmp_feedback_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom plugin table, no core API exists return $wpdb->get_results( $wpdb->prepare("SELECT * FROM %i ORDER BY created_at DESC LIMIT %d", $table, $limit), ARRAY_A ); } // Deleting feedback removes what people wrote about themselves, so it asks twice: // once in the browser, and once by requiring the word to be typed. function aicmp_feedback_reset() { global $wpdb; $table = aicmp_feedback_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- clearing the plugin's own table on an explicit admin action $wpdb->query($wpdb->prepare("TRUNCATE TABLE %i", $table)); } function aicmp_feedback_count() { global $wpdb; $table = aicmp_feedback_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom plugin table return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i", $table)); } // ── CSV EXPORT ── // Must run on admin_init: by the time the page callback fires, WordPress has already // written the admin header, so Content-Disposition would be ignored and the CSV rows // would be appended to the HTML page instead of being served as a file. add_action('admin_init', 'aicmp_maybe_export_stats'); function aicmp_maybe_export_stats() { if (!isset($_GET['aicmp_export'])) { return; } if (!isset($_GET['page']) || sanitize_key(wp_unslash($_GET['page'])) !== 'aicreativa-music-player') { return; } aicmp_check_cap(); check_admin_referer('aicmp_export_csv'); $rows = aicmp_stats_by_track(1000); nocache_headers(); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=aicmp-stats-' . gmdate('Y-m-d') . '.csv'); $out = fopen('php://output', 'w'); fputcsv($out, ['Track', 'Plays', 'Completes', 'Downloads']); foreach ($rows as $r) { $t = get_post((int) $r['track_id']); fputcsv($out, [ $t ? $t->post_title : '#' . $r['track_id'], (int) $r['plays'], (int) $r['completes'], (int) $r['downloads'], ]); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- $out is the PHP output stream (php://output), not a filesystem file; WP_Filesystem cannot stream a download fclose($out); exit; } // ── REVIEW REQUEST ── // Shown only on this plugin's own dashboard, never as a site-wide notice, and only // after the plugin has actually been used for a while. Asks for a review, never for // a specific star rating - WordPress.org treats that as rating manipulation. function aicmp_review_should_show($plays) { if (get_option('aicmp_review_done')) { return false; } if ((int) get_option('aicmp_review_snooze', 0) > time()) { return false; } $installed = (int) get_option('aicmp_installed_at', 0); if (!$installed) { // Upgrades from before this option existed: start the clock now. add_option('aicmp_installed_at', time()); return false; } if ((time() - $installed) < AICMP_REVIEW_MIN_DAYS * DAY_IN_SECONDS) { return false; } return $plays >= AICMP_REVIEW_MIN_PLAYS; } function aicmp_allow_wporg_redirect($hosts) { $hosts[] = 'wordpress.org'; return $hosts; } function aicmp_review_action_url($action) { return wp_nonce_url( admin_url('admin.php?page=aicreativa-music-player&aicmp_review=' . $action), 'aicmp_review' ); } function aicmp_review_card($plays) { ?>

' . esc_html(number_format_i18n($plays)) . '' ); ?>

' . esc_html__('Statistics have been reset.', 'aicreativa-music-player') . '

'; } $totals = aicmp_stats_totals(); $rows = aicmp_stats_by_track(50); $show_review = aicmp_review_should_show($totals['plays']); $range = aicmp_stats_range(); $track_count = wp_count_posts('aicmp_track')->publish ?? 0; ?>

v

$totals['plays'], __('Completes', 'aicreativa-music-player') => $totals['completes'], __('Downloads', 'aicreativa-music-player') => $totals['downloads'], ]; foreach ($cards as $label => $val): ?>

post_title) : esc_html('#' . $r['track_id']); ?>

= 300 && $w <= 2000) ? $w : 952); update_option('aicmp_show_download', isset($_POST['aicmp_show_download']) ? 1 : 0); update_option('aicmp_load_inter', isset($_POST['aicmp_load_inter']) ? 1 : 0); $op = absint(wp_unslash($_POST['aicmp_skin2_opacity'] ?? 100)); update_option('aicmp_skin2_opacity', min(100, $op)); update_option('aicmp_continuous', isset($_POST['aicmp_continuous']) ? 1 : 0); update_option('aicmp_dock_playlist', absint(wp_unslash($_POST['aicmp_dock_playlist'] ?? 0))); update_option('aicmp_promo_open', isset($_POST['aicmp_promo_open']) ? 1 : 0); $fb_mail = trim(sanitize_email(wp_unslash($_POST['aicmp_feedback_email'] ?? ''))); update_option('aicmp_feedback_email', is_email($fb_mail) ? $fb_mail : ''); update_option('aicmp_autoplay', isset($_POST['aicmp_autoplay']) ? 1 : 0); $ap_mode = sanitize_text_field(wp_unslash($_POST['aicmp_autoplay_mode'] ?? 'interaction')); update_option('aicmp_autoplay_mode', in_array($ap_mode, ['interaction', 'muted'], true) ? $ap_mode : 'interaction'); echo '

' . esc_html__('Settings saved.', 'aicreativa-music-player') . '

'; } $accent = get_option('aicmp_accent_color', '#73f5fe'); $bg = get_option('aicmp_bg_color', '#1f1e1e'); $text = get_option('aicmp_text_color', '#b3dcea'); $buy = get_option('aicmp_buy_label', 'BUY'); $width = get_option('aicmp_player_width', 952); $download = get_option('aicmp_show_download', 1); $inter = get_option('aicmp_load_inter', 1); $dock_op = max(0, min(100, (int) get_option('aicmp_skin2_opacity', 100))); $cont = get_option('aicmp_continuous', 0); $dock_pl = (int) get_option('aicmp_dock_playlist', 0); $fb_mail = get_option('aicmp_feedback_email', ''); $fb_open = get_option('aicmp_promo_open', 0); $pl_list = get_posts([ 'post_type' => 'aicmp_playlist', 'post_status' => 'publish', 'numberposts' => 100, 'orderby' => 'title', 'order' => 'ASC', 'suppress_filters' => false, ]); $autoplay = get_option('aicmp_autoplay', 0); $ap_mode = get_option('aicmp_autoplay_mode', 'interaction'); ?>

v

[aicmp_track id="X"]
[aicmp_playlist id="X"]
skin="2"
skin="3"

ID, '_aicmp_mp3_url', true); $artist = get_post_meta($post->ID, '_aicmp_artist', true); $buy_url = get_post_meta($post->ID, '_aicmp_buy_url', true); $artwork_id = get_post_meta($post->ID, '_aicmp_artwork_id', true); $artwork_url = $artwork_id ? wp_get_attachment_image_url($artwork_id, 'medium') : ''; ?>

🎵

post_status === 'publish') { aicmp_shortcode_field('[aicmp_track id="' . $post->ID . '"]', __('Skin 1 — classic inline player:', 'aicreativa-music-player')); aicmp_shortcode_field('[aicmp_track id="' . $post->ID . '" skin="2"]', __('Skin 2 — docked bottom bar:', 'aicreativa-music-player')); } else { echo '

' . esc_html__('Publish to get the shortcode.', 'aicreativa-music-player') . '

'; } } function aicmp_playlist_meta_box($post) { wp_nonce_field('aicmp_playlist_meta_nonce', 'aicmp_playlist_nonce'); $selected = get_post_meta($post->ID, '_aicmp_track_ids', true) ?: []; $selected = array_map('intval', (array) $selected); $tracks = get_posts(['post_type'=>'aicmp_track','numberposts'=>-1,'orderby'=>'title','order'=>'ASC']); if (empty($tracks)) { echo '

' . esc_html__('No tracks found. Add tracks first.', 'aicreativa-music-player') . '

'; return; } $by_id = []; foreach ($tracks as $t) { $by_id[$t->ID] = $t; } // keep saved order, drop deleted tracks $selected = array_values(array_filter($selected, function($tid) use ($by_id) { return isset($by_id[$tid]); })); ?>

post_title); ?>

⠇ post_title); ?>
post_status === 'publish') { aicmp_shortcode_field('[aicmp_playlist id="' . $post->ID . '"]', __('Skin 1 — classic inline player:', 'aicreativa-music-player')); aicmp_shortcode_field('[aicmp_playlist id="' . $post->ID . '" skin="2"]', __('Skin 2 — docked bottom bar:', 'aicreativa-music-player')); aicmp_shortcode_field('[aicmp_playlist id="' . $post->ID . '" skin="3"]', __('Skin 3 — promo, with feedback form:', 'aicreativa-music-player')); } else { echo '

' . esc_html__('Publish to get the shortcode.', 'aicreativa-music-player') . '

'; } } // ── SAVE META ── add_action('save_post_aicmp_track', function($post_id) { if (!isset($_POST['aicmp_track_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['aicmp_track_nonce'])), 'aicmp_track_meta_nonce')) return; if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (!current_user_can('edit_post', $post_id)) return; if (isset($_POST['aicmp_mp3_url'])) update_post_meta($post_id, '_aicmp_mp3_url', esc_url_raw(wp_unslash($_POST['aicmp_mp3_url']))); if (isset($_POST['aicmp_artist'])) update_post_meta($post_id, '_aicmp_artist', sanitize_text_field(wp_unslash($_POST['aicmp_artist']))); if (isset($_POST['aicmp_buy_url'])) update_post_meta($post_id, '_aicmp_buy_url', esc_url_raw(wp_unslash($_POST['aicmp_buy_url']))); if (isset($_POST['aicmp_artwork_id'])) update_post_meta($post_id, '_aicmp_artwork_id', intval($_POST['aicmp_artwork_id'])); }); // The press release lives on the playlist rather than in a separate section: // a promo is the same set of tracks, just sent out earlier and with a note. function aicmp_promo_meta_box($post) { wp_nonce_field('aicmp_promo_meta_nonce', 'aicmp_promo_nonce'); $head = get_post_meta($post->ID, '_aicmp_promo_head', true); $body = get_post_meta($post->ID, '_aicmp_promo_text', true); ?>

ID, '_aicmp_promo_file', true); $flbl = get_post_meta($post->ID, '_aicmp_promo_file_label', true); ?>

rgba string with alpha (for softer secondary text) function aicmp_hex_rgba($hex, $a) { $hex = ltrim((string) $hex, '#'); if (strlen($hex) === 3) { $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; } if (strlen($hex) !== 6) { $hex = 'ffffff'; } $r = hexdec(substr($hex,0,2)); $g = hexdec(substr($hex,2,2)); $b = hexdec(substr($hex,4,2)); return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $a . ')'; } // Lighten a hex color toward white by a fraction (0..1). Returns #rrggbb. function aicmp_lighten($hex, $frac) { $hex = ltrim((string) $hex, '#'); if (strlen($hex) === 3) { $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; } if (strlen($hex) !== 6) { $hex = '1f1e1e'; } $frac = max(0, min(1, (float) $frac)); $out = '#'; for ($i = 0; $i < 3; $i++) { $c = hexdec(substr($hex, $i * 2, 2)); $c = (int) round($c + (255 - $c) * $frac); $out .= str_pad(dechex(max(0, min(255, $c))), 2, '0', STR_PAD_LEFT); } return $out; } // Return a readable icon color (#141414 or #f4f4f4) for a given background hex. // Shifts a colour away from itself toward whichever of black/white contrasts with it. // aicmp_lighten() always moves toward white, which disappears on a light background; // this keeps surfaces and dividers visible whichever background the user picks. function aicmp_shade($hex, $frac) { $hex = ltrim((string) $hex, '#'); if (strlen($hex) === 3) { $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; } if (strlen($hex) !== 6) { $hex = '1f1e1e'; } $target = ltrim(aicmp_contrast_color('#' . $hex), '#'); $frac = max(0, min(1, (float) $frac)); $out = '#'; for ($i = 0; $i < 3; $i++) { $c = hexdec(substr($hex, $i * 2, 2)); $t = hexdec(substr($target, $i * 2, 2)); $c = (int) round($c + ($t - $c) * $frac); $out .= str_pad(dechex(max(0, min(255, $c))), 2, '0', STR_PAD_LEFT); } return $out; } function aicmp_contrast_color($hex) { $hex = ltrim((string) $hex, '#'); if (strlen($hex) === 3) { $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; } if (strlen($hex) !== 6) { $hex = 'ffffff'; } $r = hexdec(substr($hex,0,2)); $g = hexdec(substr($hex,2,2)); $b = hexdec(substr($hex,4,2)); $yiq = ($r * 299 + $g * 587 + $b * 114) / 1000; return $yiq >= 140 ? '#141414' : '#f4f4f4'; } // ── FRONTEND ASSET HANDLES (inline CSS/JS attach to these) ── add_action('wp_enqueue_scripts', function () { wp_register_style('aicmp-player', false, [], AICMP_VERSION); wp_register_script('aicmp-player', false, [], AICMP_VERSION, true); // Load player assets in the head whenever a player will appear: either the // page carries a shortcode, or the site-wide docked bar is switched on. In the // head, so the inline CSS applies reliably across themes and environments. $aicmp_needed = aicmp_sitewide_dock_id() > 0; if (!$aicmp_needed && is_singular()) { $aicmp_post = get_post(); $aicmp_needed = $aicmp_post && (has_shortcode($aicmp_post->post_content, 'aicmp_track') || has_shortcode($aicmp_post->post_content, 'aicmp_playlist')); } if ($aicmp_needed) { if (get_option('aicmp_load_inter', 1)) { wp_enqueue_style( 'aicmp-inter', 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap', [], AICMP_VERSION ); } wp_enqueue_style('aicmp-player'); wp_add_inline_style('aicmp-player', aicmp_build_css()); wp_enqueue_script('aicmp-player'); aicmp_enqueue_knob_js(); } }); // ── ARC KNOB (shared, printed once before the player instances) ── // Keeps the native range input in the DOM as the accessible control and // paints an SVG arc over it, so every existing 'input' listener still works. function aicmp_enqueue_knob_js() { static $done = false; if ($done) { return; } $done = true; wp_add_inline_script('aicmp-player', aicmp_knob_js()); if (get_option('aicmp_continuous', 0)) { wp_add_inline_script('aicmp-player', aicmp_resume_js()); } } function aicmp_knob_js() { return <<<'AICMPKNOB' window.aicmpKnob=function(el){ if(el.__aicmpKnob)return;el.__aicmpKnob=true; var input=el.querySelector('input'),arc=el.querySelector('.aicmp-knob-val'); if(!input||!arc)return; var r=parseFloat(arc.getAttribute('r'))||17,C=2*Math.PI*r,SWEEP=C*0.75,GAP=(C+1).toFixed(2); var min=parseFloat(input.min);if(isNaN(min))min=0; var max=parseFloat(input.max);if(isNaN(max))max=1; var step=parseFloat(input.step)||0.01,span=max-min||1; function paint(){ var p=(parseFloat(input.value)-min)/span; if(isNaN(p))p=0;p=Math.max(0,Math.min(1,p)); arc.setAttribute('stroke-dasharray',(SWEEP*p).toFixed(2)+' '+GAP); } function set(v){ v=Math.max(min,Math.min(max,v)); v=Math.round((v-min)/step)*step+min; v=parseFloat(v.toFixed(4)); if(v===parseFloat(input.value))return; input.value=v;paint(); input.dispatchEvent(new Event('input',{bubbles:true})); input.dispatchEvent(new Event('change',{bubbles:true})); } paint(); input.addEventListener('input',paint); var dragging=false,startY=0,startV=0; el.addEventListener('pointerdown',function(e){ dragging=true;startY=e.clientY;startV=parseFloat(input.value)||min; try{el.setPointerCapture(e.pointerId);}catch(x){} try{input.focus({preventScroll:true});}catch(x){} e.preventDefault(); }); el.addEventListener('pointermove',function(e){ if(!dragging)return; set(startV+((startY-e.clientY)/110)*span); }); function stop(e){dragging=false;try{el.releasePointerCapture(e.pointerId);}catch(x){}} el.addEventListener('pointerup',stop); el.addEventListener('pointercancel',stop); el.addEventListener('wheel',function(e){ e.preventDefault(); set((parseFloat(input.value)||min)+(e.deltaY<0?1:-1)*span*0.04); },{passive:false}); el.addEventListener('dblclick',function(){ var d=parseFloat(el.getAttribute('data-def')); if(!isNaN(d))set(d); }); }; window.aicmpKnobAll=function(root){ var scope=root||document; var list=scope.querySelectorAll('.aicmp-knob'); for(var i=0;i'); // Skin 2 docks over the page, so its panels can be made translucent. // A blur keeps the text readable over whatever scrolls underneath. $dock_op = max(0, min(100, (int) get_option('aicmp_skin2_opacity', 100))) / 100; $dock_bg = $dock_op >= 1 ? $bg : esc_attr(aicmp_hex_rgba($bg, $dock_op)); $dock_blur = $dock_op >= 1 ? '' : 'backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);'; // v3.0.0 - every neutral is derived from the chosen background so the player // holds together on a light background too, not only on a dark one. $surface = esc_attr(aicmp_shade($bg, 0.05)); // artwork placeholders, thumbnails $edge = esc_attr(aicmp_shade($bg, 0.07)); // outer borders and dividers $muted = esc_attr(aicmp_hex_rgba(aicmp_contrast_color($bg), 0.46)); $muted2 = esc_attr(aicmp_hex_rgba(aicmp_contrast_color($bg), 0.56)); $scroll = esc_attr(aicmp_hex_rgba(aicmp_contrast_color($bg), 0.16)); $peak = esc_attr(aicmp_hex_rgba(aicmp_contrast_color($bg), 0.34)); // The compact single-track layout: cover beside the details, waveform and // controls each on their own full-width row. Used on phones, and used // regardless of screen size once the chosen width is too narrow for a cover // and a column side by side - a 300px player on a wide monitor never reaches // a viewport breakpoint, so the width has to trigger it too. $compact = " /* On a phone two side-by-side columns leave the waveform squeezed into the right half while the cover floats in a column that is taller than it needs to be. Rebuilt as a grid: cover and details share the first row, then the waveform and the controls each take the full width beneath them. display:contents lets .aicmp-inner hand its three children to the grid. */ .aicmp-outer{display:grid;grid-template-columns:auto 1fr;gap:0 11px; background:{$bg};padding:9px 12px;align-items:start} .aicmp-inner{display:contents} .aicmp-aw{grid-column:1;grid-row:1;width:auto;padding:0;align-items:flex-start} .aicmp-aw img,.aicmp-aw-ph{width:72px;height:72px} .aicmp-aw-ph{font-size:26px} .aicmp-top{grid-column:2;grid-row:1;flex-wrap:wrap;row-gap:2px;margin-bottom:0;gap:8px} .aicmp-pbtn{height:30px;width:28px} .aicmp-pbtn svg{width:22px;height:22px} .aicmp-ttl{font-size:12px;margin-bottom:2px} .aicmp-sub{font-size:10.5px} .aicmp-info{order:-1;flex:1 1 100%} .aicmp-ttl{white-space:normal} .aicmp-ww{grid-column:1 / -1;grid-row:2;height:46px;margin:6px 0 0} .aicmp-cv{height:46px} .aicmp-bot{grid-column:1 / -1;grid-row:3;gap:6px;margin-top:4px} .aicmp-knob svg{width:26px;height:26px} .aicmp-knob{gap:2px;padding:1px} "; $compact_pl = " .aicmp-pl-top{flex-wrap:wrap;row-gap:10px} .aicmp-pl-info{order:-1;flex:1 1 100%} .aicmp-pl-ttl{white-space:normal} /* the title takes the whole first row, so the counter has to be pushed to the right edge of the second row instead of trailing the transport buttons */ .aicmp-pl-tb{margin-left:auto;line-height:1.45} .aicmp-pl-head{padding:12px 14px} .aicmp-pl-head .aicmp-ww{height:64px} .aicmp-pl-head .aicmp-cv{height:64px} .aicmp-pl-art{width:48px;height:48px} .aicmp-pl-art-ph{width:48px;height:48px;font-size:20px} .aicmp-pl-bot{gap:6px} .aicmp-pl-item{padding:9px 12px} "; $compact_always = ($width <= 460) ? $compact . "\n" . $compact_pl : ''; return " .aicmp-outer,.aicmp-pl-outer,.aicmp2-dock,.aicmp-player-wrap{font-family:{$font};-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} /* Both skins sit centred in whatever column they are placed in. For a left-aligned player, change margin-inline to 0 auto 0 0 on both rules. */ .aicmp-player-wrap{margin-inline:auto} .aicmp-outer{margin-inline:auto} .aicmp-outer{display:flex;align-items:stretch;max-width:{$width}px;border-radius:12px;overflow:hidden;border:1px solid {$bg}} .aicmp-aw{flex-shrink:0;width:182px;background:{$bg};display:flex;align-items:center;justify-content:center;padding:13px;box-sizing:border-box} .aicmp-aw img{width:156px;height:156px;object-fit:cover;border-radius:2px;display:block} .aicmp-aw-ph{width:156px;height:156px;background:{$surface};border-radius:2px;display:flex;align-items:center;justify-content:center;font-size:52px} .aicmp-inner{background:{$bg};padding:13px 18px;flex:1;min-width:0;display:flex;flex-direction:column;justify-content:space-between} .aicmp-top{display:flex;align-items:center;gap:10px;margin-bottom:9px} .aicmp-controls{display:flex;align-items:center;gap:6px;flex-shrink:0} .aicmp-pbtn,.aicmp-pl-pbtn{width:32px;height:36px;padding:0;border-radius:8px;background:none;border:none;color:{$ico};cursor:pointer;display:flex;align-items:center;justify-content:center;transition:color .15s,transform .1s} .aicmp-pbtn svg,.aicmp-pl-pbtn svg{width:26px;height:26px;fill:currentColor} .aicmp-pbtn:hover,.aicmp-pl-pbtn:hover{color:{$text}} .aicmp-pbtn:active,.aicmp-pl-pbtn:active{transform:scale(.94)} .aicmp-skipbtn,.aicmp-pl-skipbtn{width:24px;height:32px;padding:0;border-radius:6px;background:none;border:none;color:{$ico_dim};cursor:pointer;display:flex;align-items:center;justify-content:center;transition:color .15s} .aicmp-skipbtn svg,.aicmp-pl-skipbtn svg{width:17px;height:17px;fill:currentColor;stroke:none} .aicmp-skipbtn:hover,.aicmp-pl-skipbtn:hover{color:{$ico}} .aicmp-skipbtn:disabled,.aicmp-pl-skipbtn:disabled{opacity:0.26;cursor:default} .aicmp-skipbtn:disabled:hover,.aicmp-pl-skipbtn:disabled:hover{color:{$ico_dim}} .aicmp-info{flex:1;min-width:0} .aicmp-ttl{color:{$text};font-size:12.5px;font-weight:600;margin:0 0 2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .aicmp-sub{color:{$text_soft};font-size:11px;margin:0} .aicmp-tb{color:{$text_soft};font-size:11px;flex-shrink:0;text-align:right} .aicmp-ww{height:72px;cursor:pointer;margin-bottom:4px} .aicmp-cv{width:100%;height:72px;display:block;background:{$bg}} .aicmp-bot{display:flex;align-items:center;justify-content:space-between;gap:8px} .aicmp-vr{display:flex;align-items:center;gap:6px;flex-shrink:0} .aicmp-actions{display:flex;align-items:center;gap:6px;flex-shrink:0} .aicmp-buy{background:{$bg};color:{$text};font-size:9.5px;font-weight:700;padding:3px 8px;border-radius:5px;text-decoration:none;letter-spacing:.4px;line-height:1.5;border:1px solid {$text};box-sizing:border-box} .aicmp-buy:hover{background:{$btn_hover}} .aicmp-dl{background:{$bg};color:{$text};font-size:9.5px;font-weight:600;padding:3px 8px;border-radius:5px;text-decoration:none;letter-spacing:.4px;line-height:1.5;border:1px solid {$text};box-sizing:border-box;display:{$dl_disp}} .aicmp-dl:hover{background:{$btn_hover}} .aicmp-repeat,.aicmp-pltog{width:30px;height:30px;padding:0;border-radius:6px;background:none;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:1;color:{$ico_dim};transition:color .15s;flex-shrink:0} .aicmp-repeat svg,.aicmp-pltog svg{width:16px;height:16px;stroke:currentColor;fill:none} .aicmp-repeat:hover,.aicmp-pltog:hover{color:{$ico}} .aicmp-repeat.active{color:{$text}} .aicmp-pl-outer{border-radius:12px;overflow:hidden;border:1px solid {$edge}} .aicmp-pl-head{background:{$bg};padding:16px 20px} .aicmp-pl-head .aicmp-ww{height:80px} .aicmp-pl-head .aicmp-cv{height:80px} .aicmp-pl-top{display:flex;align-items:center;gap:10px;margin-bottom:12px} .aicmp-pl-art{width:64px;height:64px;object-fit:cover;border-radius:6px;flex-shrink:0} .aicmp-pl-art-ph{width:64px;height:64px;background:{$surface};border-radius:6px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:28px} .aicmp-pl-info{flex:1;min-width:0} .aicmp-pl-ttl{color:{$text};font-size:12.5px;font-weight:600;margin:0 0 2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .aicmp-pl-sub{color:{$text_soft};font-size:11px;margin:0} .aicmp-pl-tb{color:{$text_soft};font-size:11px;flex-shrink:0;text-align:right} .aicmp-pl-bot{display:flex;align-items:center;justify-content:space-between;gap:8px} .aicmp-pl-vr{display:flex;align-items:center;gap:4px;flex-shrink:0} .aicmp-pl-vr .aicmp-knob{margin-right:6px} .aicmp-shuf.active{color:{$text}} .aicmp-pltog[aria-expanded=\"true\"]{color:{$text}} .aicmp-pl-listwrap{display:grid;grid-template-rows:1fr;transition:grid-template-rows .3s ease} .aicmp-pl-listwrap.aicmp-collapsed{grid-template-rows:0fr} .aicmp-pl-listwrap>.aicmp-pl-list{overflow:hidden;min-height:0} .aicmp-pl-list{background:{$bg}} .aicmp-pl-item{display:flex;align-items:center;gap:10px;padding:10px 16px;cursor:pointer;background:{$bg};border-bottom:0.5px solid {$pl_border};user-select:none;transition:color .15s} .aicmp-pl-item:hover{background:{$bg}} .aicmp-pl-item.active{background:{$bg}} .aicmp-pl-item:hover .aicmp-pl-it{color:{$accent}} .aicmp-pl-item.active .aicmp-pl-it,.aicmp-pl-item.active .aicmp-pl-is,.aicmp-pl-item.active .aicmp-pl-num,.aicmp-pl-item.active .aicmp-pl-dur,.aicmp-pl-item.active .aicmp-pl-drag{color:{$accent}} .aicmp-pl-it,.aicmp-pl-is,.aicmp-pl-num,.aicmp-pl-dur{transition:color .15s} .aicmp-pl-item.drag-over{border-top:2px solid {$accent}} .aicmp-pl-item.dragging{opacity:0.4} .aicmp-pl-drag{color:{$muted};font-size:11px;cursor:grab;flex-shrink:0} .aicmp-pl-num{color:{$muted};width:15px;font-size:10px;text-align:center;flex-shrink:0} .aicmp-pl-ia{width:30px;height:30px;background:{$surface};border-radius:3px;flex-shrink:0;object-fit:cover} .aicmp-pl-ia-ph{width:30px;height:30px;background:{$surface};border-radius:3px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:13px} .aicmp-pl-ii{flex:1;min-width:0} .aicmp-pl-it{font-size:11.5px;font-weight:500;color:{$text};white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .aicmp-pl-is{font-size:9.5px;color:{$muted2}} .aicmp-pl-dur{font-size:9.5px;color:{$muted2};flex-shrink:0} .aicmp2-dock{--a-art:clamp(40px,2.7vw,50px);--a-play:clamp(39px,2.65vw,48px);--a-btn:clamp(26px,1.8vw,32px);--a-tbtn:clamp(24px,1.6vw,29px);--a-gap:clamp(6px,0.65vw,10px);--a-gap2:clamp(3px,0.35vw,5px);--a-pady:clamp(9px,0.8vw,13px);--a-padx:clamp(10px,1.1vw,16px);--a-wave:clamp(46px,3.6vw,58px);--a-title:clamp(12px,0.78vw,13px);--a-sub:clamp(10.5px,0.75vw,11.5px);position:fixed;left:0;right:0;bottom:0;z-index:99999} .aicmp2-pl{max-height:min(50vh,380px);overflow:hidden;background:{$dock_bg};{$dock_blur}border-top:1px solid {$edge};transition:max-height .32s ease} .aicmp2-pl.aicmp-collapsed{max-height:0;border-top:none} .aicmp2-pl-scroll{max-height:min(50vh,380px);overflow-y:auto;scrollbar-width:thin;scrollbar-color:{$scroll} transparent} .aicmp2-pl-scroll::-webkit-scrollbar{width:10px} .aicmp2-pl-scroll::-webkit-scrollbar-track{background:transparent} .aicmp2-pl-scroll::-webkit-scrollbar-thumb{background:{$scroll};border-radius:5px;border:2px solid transparent;background-clip:padding-box} .aicmp2-vis{max-height:min(52vh,400px);overflow:hidden;background:{$dock_bg};{$dock_blur}border-top:1px solid {$edge};transition:max-height .32s ease} .aicmp2-vis.aicmp-collapsed{max-height:0;border-top:none} .aicmp2-vis-inner{padding:14px 16px} .aicmp2-viscv{width:100%;height:clamp(160px,26vh,280px);display:block;border-radius:6px;background:transparent} .aicmp2-bar{display:flex;flex-wrap:nowrap;align-items:center;gap:var(--a-gap);background:{$dock_bg};{$dock_blur}border-top:1px solid {$edge};padding:var(--a-pady) var(--a-padx);box-sizing:border-box} .aicmp2-art{width:var(--a-art);height:var(--a-art);border-radius:7px;object-fit:cover;flex-shrink:0;background:{$surface}} .aicmp2-art-ph{display:flex;align-items:center;justify-content:center;font-size:26px} .aicmp2-transport{display:flex;align-items:center;gap:1px;flex-shrink:0} .aicmp2-btn{border:none;background:none;padding:0;color:{$ico_dim};width:var(--a-tbtn);height:var(--a-tbtn);border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:color .15s,transform .1s} .aicmp2-btn:hover{color:{$ico}} .aicmp2-btn:active{transform:scale(.94)} .aicmp2-btn:disabled{opacity:.28;cursor:default} .aicmp2-btn:disabled:hover{color:{$ico_dim}} .aicmp2-btn svg{width:52%;height:52%;fill:currentColor} .aicmp2-play{width:var(--a-play);height:var(--a-play);background:none;color:{$ico}} .aicmp2-play svg{width:62%;height:62%} .aicmp2-play:hover{color:{$text}} .aicmp2-meta{min-width:0;flex:0 1 auto;max-width:clamp(150px,20vw,330px);overflow:hidden} .aicmp2-title{font-weight:600;letter-spacing:-.01em;color:{$text};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:var(--a-title);margin:0} .aicmp2-artist{color:{$text_soft};font-size:var(--a-sub);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0} .aicmp2-wave{flex:1 1 0;min-width:110px;height:var(--a-wave);cursor:pointer} .aicmp2-cv{width:100%;height:var(--a-wave);display:block;background:transparent} .aicmp2-info{flex-shrink:0;text-align:right;line-height:1.3;min-width:46px} .aicmp2-counter{color:{$ico_dim};font-size:calc(var(--a-sub) - 1px);letter-spacing:.02em;font-variant-numeric:tabular-nums} .aicmp2-time{color:{$text_soft};font-size:var(--a-sub);font-variant-numeric:tabular-nums} .aicmp2-vol{display:flex;align-items:center;gap:var(--a-gap2);flex-shrink:0} .aicmp2-vol::before,.aicmp2-actions::before{content:'';flex-shrink:0;width:1px;height:20px;background:{$knob_trk};margin-right:calc(var(--a-gap) - var(--a-gap2))} .aicmp2-modes{display:flex;align-items:center;gap:var(--a-gap2);flex-shrink:0} .aicmp2-mode{background:none;border:none;cursor:pointer;padding:0;width:var(--a-btn);height:var(--a-btn);border-radius:6px;display:flex;align-items:center;justify-content:center;opacity:1;transition:color .15s;color:{$ico_dim};flex-shrink:0} .aicmp2-mode:hover{color:{$ico}} .aicmp2-mode.active{color:{$text}} .aicmp2-mode svg{width:52%;height:52%} .aicmp2-actions{display:flex;align-items:center;gap:clamp(4px,0.4vw,6px);flex-shrink:0} .aicmp2-act{border:1px solid {$text};color:{$text};background:none;border-radius:5px;padding:3px 7px;font-size:clamp(8px,0.6vw,9.5px);font-weight:700;line-height:1.5;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:4px;transition:background .15s,color .15s;white-space:nowrap;box-sizing:border-box} .aicmp2-act:hover{background:{$text};color:{$bg}} .aicmp2-act svg{width:9px;height:9px} .aicmp2-dl{display:{$dl_flex}} .aicmp2-toggle{background:none;border:none;cursor:pointer;padding:0;width:var(--a-btn);height:var(--a-btn);border-radius:6px;flex-shrink:0;display:flex;flex-direction:column;gap:4px;align-items:center;justify-content:center} .aicmp-relaunch{position:fixed;right:16px;bottom:16px;z-index:99998;width:44px;height:44px;border-radius:50%;border:1px solid {$edge};background:{$bg};color:{$text};cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;box-shadow:0 4px 18px rgba(0,0,0,.45);transition:transform .15s,color .15s;font-family:{$font}} .aicmp-relaunch:hover{transform:scale(1.06);color:{$accent}} .aicmp-relaunch svg{width:17px;height:17px;fill:currentColor;margin-left:2px} @media(max-width:600px){.aicmp-relaunch{right:12px;bottom:12px;width:40px;height:40px}} .aicmp2-close{background:none;border:none;cursor:pointer;padding:0;width:var(--a-btn);height:var(--a-btn);border-radius:6px;display:flex;align-items:center;justify-content:center;color:{$ico_dim};transition:color .15s;flex-shrink:0} .aicmp2-close:hover{color:{$text}} .aicmp2-close svg{width:52%;height:52%} .aicmp2-toggle span{display:block;width:16px;height:2px;background:{$ico_dim};border-radius:2px;transition:.25s} .aicmp2-toggle:hover span{background:{$ico}} .aicmp2-toggle.open span{background:{$text}} .aicmp2-pl .aicmp-pl-item{padding:10px 18px} .aicmp2-art{cursor:zoom-in} .aicmp2-art-ph{cursor:default} .aicmp2-lightbox{position:fixed;inset:0;z-index:100000;background:rgba(0,0,0,.85);display:none;align-items:center;justify-content:center;cursor:zoom-out;padding:24px} .aicmp2-lightbox.open{display:flex} .aicmp2-lightbox img{max-width:min(90vw,520px);max-height:85vh;width:auto;height:auto;border-radius:10px;box-shadow:0 12px 48px rgba(0,0,0,.6)} .aicmp2-single .aicmp2-toggle{display:none} .aicmp2-single .aicmp2-counter{display:none} .aicmp2-single .aicmp2-transport .aicmp2-btn:not(.aicmp2-play){display:none} .aicmp2-single .aicmp2-pl{display:none} @media(max-width:1100px){ .aicmp2-bar{flex-wrap:wrap;row-gap:clamp(9px,1.4vw,14px)} .aicmp2-art{order:0} .aicmp2-transport{order:1} .aicmp2-meta{order:2;flex:1;min-width:120px;max-width:none} .aicmp2-toggle{order:3;margin-left:auto} .aicmp2-close{order:3} .aicmp2-wave{order:4;flex-basis:100%;width:100%;min-width:0} .aicmp2-info{order:5;text-align:left;min-width:0} .aicmp2-vol{order:6} .aicmp2-modes{order:7;margin-left:auto} .aicmp2-vol::before,.aicmp2-actions::before{display:none} .aicmp2-actions{order:8} } @media(max-width:600px){ .aicmp2-dock{--a-art:clamp(42px,12vw,52px);--a-play:clamp(44px,12.5vw,52px);--a-btn:clamp(30px,8vw,34px);--a-tbtn:clamp(34px,9.5vw,40px)} .aicmp2-counter{display:none} .aicmp2-vol{order:5;gap:5px} .aicmp2-info{order:6} .aicmp2-act{padding:4px 8px} .aicmp2-toggle{order:9;margin-left:0} .aicmp2-close{order:10} .aicmp2-toggle span{width:16px} } .aicmp2-pl .aicmp-pl-list,.aicmp2-pl .aicmp-pl-item,.aicmp2-pl .aicmp-pl-item:hover,.aicmp2-pl .aicmp-pl-item.active{background:transparent} .aicmp-knob{position:relative;display:flex;flex-direction:column;align-items:center;gap:3px;flex-shrink:0;user-select:none;touch-action:none;cursor:ns-resize;padding:2px} .aicmp-knob svg{width:34px;height:34px;display:block} .aicmp-knob circle{fill:none;stroke-width:3;stroke-linecap:round} .aicmp-knob-bg{stroke:{$knob_trk}} .aicmp-knob-val{stroke:{$accent}} .aicmp-knob-lbl{font-size:8.5px;font-weight:600;letter-spacing:1.4px;line-height:1;color:{$ico_dim};pointer-events:none} .aicmp-knob input{position:absolute;left:0;top:0;width:100%;height:100%;margin:0;padding:0;opacity:0;pointer-events:none;-webkit-appearance:none;appearance:none;background:none} .aicmp-knob:hover .aicmp-knob-lbl{color:{$ico}} .aicmp-knob:focus-within .aicmp-knob-lbl{color:{$text}} .aicmp-knob:focus-within .aicmp-knob-val{stroke-width:4} .aicmp2-dock .aicmp-knob-val{stroke:{$text}} .aicmp2-dock .aicmp-knob-lbl{color:{$text_soft}} .aicmp2-dock .aicmp-knob:hover .aicmp-knob-lbl{color:{$text}} .aicmp2-vol{gap:clamp(5px,0.55vw,9px)} .aicmp2-sens{display:none} .aicmp2-sens.aicmp-on{display:flex} .aicmp2-vol .aicmp-knob svg{width:clamp(26px,2vw,31px);height:clamp(26px,2vw,31px)} .aicmp2-vol .aicmp-knob-lbl{font-size:7.5px;letter-spacing:1.1px} @media(max-width:600px){ .aicmp-knob svg{width:34px;height:34px} .aicmp2-vol .aicmp-knob svg{width:34px;height:34px} } /* ── SKIN 3: PROMO ── */ .aicmp-promo .aicmp-buy{display:none !important} .aicmp-promo .aicmp-pl-bot .aicmp-dl{display:none} .aicmp-pl-dl{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:26px;height:26px;padding:0;border:1px solid {$edge};border-radius:6px;color:{$muted2};background:none;margin-left:4px;transition:color .15s,border-color .15s} .aicmp-pl-dl svg{width:13px;height:13px} .aicmp-pl-item:hover .aicmp-pl-dl{color:{$text};border-color:{$text}} .aicmp-promo .aicmp-dl{opacity:.3;pointer-events:none;filter:grayscale(1)} .aicmp-promo .aicmp-pl-dl{border-style:dashed} .aicmp-promo.aicmp-open .aicmp-pl-dl{border-style:solid} .aicmp-promo.aicmp-open .aicmp-dl{opacity:1;pointer-events:auto} .aicmp-release{margin-top:10px;background:{$bg};border-radius:12px;padding:18px 18px 4px;color:{$text_soft}} .aicmp-release-h{margin:0 0 10px;color:{$text};font-size:14px;font-weight:600;letter-spacing:-.01em} .aicmp-release-b p{margin:0 0 12px;font-size:13px;line-height:1.65} .aicmp-press{display:inline-flex;align-items:center;gap:8px;text-decoration:none; border:1px solid {$edge};border-radius:6px;padding:9px 15px;color:{$text}; font-size:10.5px;letter-spacing:.16em;text-transform:uppercase; transition:border-color .15s,background .15s} .aicmp-press:hover{border-color:{$text};background:{$surface}} .aicmp-press svg{width:14px;height:14px;flex-shrink:0} .aicmp-panel{margin-top:10px;background:{$bg};border-radius:12px;overflow:hidden;color:{$text_soft};font-size:13px} .aicmp-tabs{display:flex;gap:2px;padding:0 14px} .aicmp-tab{appearance:none;background:none;border:none;border-bottom:2px solid transparent;color:{$muted2};font:inherit;font-size:11px;letter-spacing:.14em;text-transform:uppercase;padding:14px 12px 11px;cursor:pointer;transition:color .15s,border-color .15s} .aicmp-tab:hover{color:{$text}} .aicmp-tab[aria-selected=\"true\"]{color:{$text};border-bottom-color:{$accent}} .aicmp-pane{padding:18px 16px 20px;display:grid;gap:14px} .aicmp-pane[hidden]{display:none} .aicmp-f{display:grid;gap:6px} .aicmp-f label{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:{$muted2}} .aicmp-in{width:100%;box-sizing:border-box;background:{$surface};border:1px solid {$edge};border-radius:6px;color:{$text};font:inherit;font-size:13px;padding:9px 11px;transition:border-color .15s} .aicmp-in:focus{outline:none;border-color:{$accent}} .aicmp-in::placeholder{color:{$muted}} select.aicmp-in{cursor:pointer;color-scheme:{$scheme}; -webkit-appearance:none;-moz-appearance:none;appearance:none; padding-right:34px; background-image:url(\"data:image/svg+xml,{$chev}\"); background-repeat:no-repeat;background-position:right 11px center;background-size:15px 15px} select.aicmp-in option{background:{$surface};color:{$text}} select.aicmp-in::-ms-expand{display:none} textarea.aicmp-in{min-height:84px;resize:vertical;line-height:1.55} .aicmp-two{display:grid;grid-template-columns:1fr 1fr;gap:14px} .aicmp-stars{display:flex;gap:3px} .aicmp-star{appearance:none;background:none;border:none;padding:2px;cursor:pointer;line-height:0;color:{$knob_trk};transition:color .12s,transform .12s} .aicmp-star svg{width:22px;height:22px;fill:currentColor;display:block} .aicmp-star:hover{transform:translateY(-1px)} .aicmp-star.on{color:{$accent}} .aicmp-row{display:flex;align-items:center;gap:14px;flex-wrap:wrap} .aicmp-btn{appearance:none;border:1px solid {$text};background:none;color:{$text};font:inherit;font-size:10.5px;letter-spacing:.18em;text-transform:uppercase;padding:11px 20px;border-radius:6px;cursor:pointer;transition:background .18s,color .18s} .aicmp-btn:hover{background:{$text};color:{$bg}} .aicmp-btn[disabled]{opacity:.45;cursor:default} .aicmp-btn[disabled]:hover{background:none;color:{$text}} .aicmp-hint{font-size:11.5px;color:{$muted2};margin:0} .aicmp-msg{font-size:12px;margin:0;min-height:1em} .aicmp-msg.ok{color:{$accent}} .aicmp-msg.err{color:#ff6b4a} @media(max-width:560px){ .aicmp-two{grid-template-columns:1fr} .aicmp-pane{padding:16px 13px 18px} } /* Last on purpose: at equal specificity the later rule wins, so the compact overrides have to come after every base rule they replace. */ @media(max-width:600px){ {$compact} {$compact_pl} } {$compact_always} "; } // ── WAVEFORM JS (shared) ── // Prints waveform JS for one player instance. All dynamic values are escaped with esc_js(). function aicmp_waveform_js($uid, $accent, $title, $is_playlist = false) { $H = $is_playlist ? 80 : 90; // Unplayed bars are derived from the background, so the waveform still reads // correctly when the user picks a light background. $bg = get_option('aicmp_bg_color', '#1f1e1e'); ?> var aicmp_off_=''; var aicmp_off2_=''; var aicmp_bars_=[]; var aicmp_BAR=300; function aicmp_seeded_(seed){ var s=0;for(var k=0;k=[]; for(var i=0;i.push(Math.max(0.05,Math.min(1,v))); } } function aicmp_placeholder_(){ aicmp_bars_=[]; for(var i=0;i.push(0.05+Math.random()*0.25); } /* A waveform never changes, so the shape is worth keeping. Analysing a six minute track means downloading it, decoding it and reading every sample - several seconds of work to draw 300 bars. Once done, the answer is cached in this browser and every later visit is instant. */ window.aicmpWf = window.aicmpWf || (function(){ var KEY = 'aicmp_wf1_'; function hash(u){ var h = 5381, i = 0; for (; i < u.length; i++) { h = ((h << 5) + h + u.charCodeAt(i)) | 0; } return (h >>> 0).toString(36); } return { read: function(url, n){ try{ var v = localStorage.getItem(KEY + hash(url)); if(!v) return null; var a = v.split(','); if(a.length !== n) return null; var out = [], i = 0; for(; i < a.length; i++){ var f = parseFloat(a[i]); if(!(f >= 0)) return null; out.push(f); } return out; }catch(e){ return null; } }, write: function(url, bars){ try{ localStorage.setItem(KEY + hash(url), bars.map(function(v){ return v.toFixed(3); }).join(',')); }catch(e){ /* Out of room: drop our own entries and carry on without a cache. */ try{ for(var i = localStorage.length - 1; i >= 0; i--){ var k = localStorage.key(i); if(k && k.indexOf(KEY) === 0) localStorage.removeItem(k); } }catch(e2){} } } }; })(); function aicmp_analyse_(url,seed,onDone){ var cached = window.aicmpWf.read(url, aicmp_BAR); if(cached){ aicmp_bars_ = cached; if(onDone)onDone(); return; } /* Decoding into an offline context at a low rate resamples on the way in, so there is roughly a quarter as much audio to decode and to read afterwards. 300 bars cannot show more detail than this anyway. */ var OAC = window.OfflineAudioContext || window.webkitOfflineAudioContext; var actx; try{ actx = OAC ? new OAC(1, 1, 11025) : new (window.AudioContext||window.webkitAudioContext)(); } catch(e){ actx = new (window.AudioContext||window.webkitAudioContext)(); } function done(){ try{ if(actx && actx.close) actx.close(); }catch(e){} if(onDone)onDone(); } fetch(url,{mode:'cors'}) .then(function(r){if(!r.ok)throw new Error('fetch');return r.arrayBuffer();}) .then(function(ab){return actx.decodeAudioData(ab);}) .then(function(decoded){ var ch=decoded.getChannelData(0),n=ch.length; var bs=Math.floor(n/aicmp_BAR)||1,nb=[]; /* Averaging every sample in a bucket and averaging 400 of them spread across it give the same bar to the eye, for a fraction of the reads. */ var step=Math.max(1,Math.floor(bs/400)); for(var i=0;i=nb.map(function(v){return Math.max(0.02,v/mx);}); window.aicmpWf.write(url, aicmp_bars_); done(); }).catch(function(){ aicmp_seeded_(seed||url); done(); }); } function aicmp_draw_(cv,c,ww,p,accent){ var W=ww.offsetWidth,H=ww.clientHeight||; if(!W)return; var dpr=window.devicePixelRatio||1; cv.width=Math.round(W*dpr);cv.height=Math.round(H*dpr); cv.style.width=W+'px';cv.style.height=H+'px'; c.setTransform(dpr,0,0,dpr,0,0); c.clearRect(0,0,W,H); var gap=0.7,minBw=0.9; // Fit the bar count to the available width so the whole track is always // visible; on narrow screens we draw fewer, evenly sampled bars. var n=Math.max(24,Math.min(aicmp_BAR,Math.floor((W+gap)/(minBw+gap)))); var bw=Math.max(minBw,(W-(n-1)*gap)/n),mid=H/2; for(var i=0;i[Math.floor(i*aicmp_BAR/n)]; if(v===undefined)v=0.05; var x=i*(bw+gap),bh=Math.pow(v,0.68)*mid*0.96,played=i/n; c.beginPath();if(c.roundRect)c.roundRect(x,mid-bh,bw,bh,0.5);else c.rect(x,mid-bh,bw,bh);c.fill(); c.fillStyle=played?accent+'88':aicmp_off2_; c.beginPath();if(c.roundRect)c.roundRect(x,mid+0.5,bw,bh*0.6,0.5);else c.rect(x,mid+0.5,bw,bh*0.6);c.fill(); } } 0, 'skin' => '1'], $atts); $id = intval($atts['id']); if (!$id) return '

' . esc_html__('AICreativa Music Player: missing track ID.', 'aicreativa-music-player') . '

'; $post = get_post($id); if (!$post || $post->post_type !== 'aicmp_track') return '

' . esc_html__('AICreativa Music Player: track not found.', 'aicreativa-music-player') . '

'; $mp3 = get_post_meta($id, '_aicmp_mp3_url', true); if (!$mp3) return '

' . esc_html__('AICreativa Music Player: no audio set for this track.', 'aicreativa-music-player') . '

'; $artist = get_post_meta($id, '_aicmp_artist', true); $buy_url = get_post_meta($id, '_aicmp_buy_url', true); $title = $post->post_title; $artwork = aicmp_get_artwork($id); $accent = get_option('aicmp_accent_color', '#73f5fe'); $bg = get_option('aicmp_bg_color', '#1f1e1e'); $text = get_option('aicmp_text_color', '#b3dcea'); $text_soft = aicmp_hex_rgba($text, 0.62); $buy_lbl = get_option('aicmp_buy_label', 'BUY'); $width = get_option('aicmp_player_width', 952); $show_dl = get_option('aicmp_show_download', 1); $autoplay = get_option('aicmp_autoplay', 0); $ap_mode = get_option('aicmp_autoplay_mode', 'interaction'); $uid = 'aicmp_' . uniqid(); if ((string) $atts['skin'] === '2') { return aicmp_render_playlist_skin2($uid, [[ 'id' => $id, 'title' => $title, 'artist' => $artist, 'mp3' => $mp3, 'buy' => $buy_url, 'artwork' => $artwork, ]], [ 'accent' => $accent, 'control' => $text, 'text' => $text, 'buy_lbl' => $buy_lbl, 'autoplay' => $autoplay, 'ap_mode' => $ap_mode, ]); } ob_start(); ?>
🎵

0:00 / 0:00
VOL
⬇ DL
(function(){ var w=document.getElementById(''); if(!w||w.getAttribute('data-aicmp-ready'))return; w.setAttribute('data-aicmp-ready','1'); var au=w.querySelector('audio'); var btn=w.querySelector('.aicmp-pbtn'),ic=w.querySelector('.aicmp-ic'); var cv=w.querySelector('.aicmp-cv'),c=cv.getContext('2d'),ww=w.querySelector('.aicmp-ww'); var vl=w.querySelector('.aicmp-vl'),curE=w.querySelector('.aicmp-cur'),durE=w.querySelector('.aicmp-dur'); var repBtn=w.querySelector('.aicmp-repeat'); var accent='',playing=false,repeatMode=0; var PLAY='',PAUSE=''; function fmt(s){if(!s||isNaN(s))return'0:00';var m=Math.floor(s/60),sec=Math.floor(s%60);return m+':'+(sec<10?'0':'')+sec;} function draw(p){aicmp_draw_(cv,c,ww,p,accent);} var repOff=repBtn.querySelector('.aicmp-rep-off'),repOne=repBtn.querySelector('.aicmp-rep-one'),repAll=repBtn.querySelector('.aicmp-rep-all'); function updateRepeat(){ repOff.style.display=repeatMode===0?'block':'none'; repOne.style.display=repeatMode===1?'block':'none'; repAll.style.display=repeatMode===2?'block':'none'; repBtn.classList.toggle('active',repeatMode>0); repBtn.title=['Repeat: Off','Repeat: One','Repeat: All'][repeatMode]; } aicmp_placeholder_();draw(0);updateRepeat(); (function(){var run=function(){aicmp_analyse_(au.src||au.getAttribute('src'),'',function(){draw(au.duration?au.currentTime/au.duration:0);});}; if('IntersectionObserver' in window){var io=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){io.disconnect();run();}});},{rootMargin:'200px'});io.observe(w);}else{run();}})(); au.addEventListener('loadedmetadata',function(){durE.textContent=fmt(au.duration);}); au.addEventListener('timeupdate',function(){draw(au.duration?au.currentTime/au.duration:0);curE.textContent=fmt(au.currentTime);}); au.addEventListener('ended',function(){ if(repeatMode===1||repeatMode===2){au.currentTime=0;au.play();} else{playing=false;ic.innerHTML=PLAY;draw(0);curE.textContent='0:00';} }); btn.addEventListener('click',function(){if(playing){au.pause();ic.innerHTML=PLAY;}else{au.play();ic.innerHTML=PAUSE;}playing=!playing;}); repBtn.addEventListener('click',function(){repeatMode=(repeatMode+1)%3;updateRepeat();}); ww.addEventListener('click',function(e){if(!au.duration)return;var r=ww.getBoundingClientRect(),p=(e.clientX-r.left)/r.width;au.currentTime=p*au.duration;draw(p);}); vl.addEventListener('input',function(){au.volume=vl.value;});au.volume=0.8; var aicmpStatUrl=''; var aicmpTid=,aicmpP=false,aicmpC=false; function aicmpStat(type){try{fetch(aicmpStatUrl,{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({track_id:aicmpTid,type:type})});}catch(e){}} au.addEventListener('timeupdate',function(){if(!aicmpP&&au.currentTime>=5){aicmpP=true;aicmpStat('play');}if(!aicmpC&&au.duration&&au.currentTime/au.duration>=0.8){aicmpC=true;aicmpStat('complete');}}); var aicmpDlE=w.querySelector('.aicmp-dl');if(aicmpDlE){aicmpDlE.addEventListener('click',function(){aicmpStat('download');});} if(!window.aicmpAP){window.aicmpAP=true; au.muted=true; au.play().then(function(){playing=true;ic.innerHTML=PAUSE;}).catch(function(){}); var aicmpUnmute=function(){au.muted=false;}; ['pointerdown','keydown','touchstart'].forEach(function(ev){window.addEventListener(ev,aicmpUnmute,{once:true,passive:true});}); var aicmpApDone=false; var aicmpApGo=function(){if(aicmpApDone)return;aicmpApDone=true;if(playing)return;au.play().then(function(){playing=true;ic.innerHTML=PAUSE;}).catch(function(){});}; ['pointerdown','keydown','touchstart','wheel','scroll'].forEach(function(ev){window.addEventListener(ev,aicmpApGo,{once:true,passive:true});}); } window.addEventListener('resize',function(){draw(au.duration?au.currentTime/au.duration:0);}); if(window.aicmpKnobAll)window.aicmpKnobAll(document.getElementById('')); })(); (function(){ var uid=''; var wrap=document.getElementById(uid); var panel=document.getElementById(uid+'_panel'); if(!wrap||!panel||panel.getAttribute('data-aicmp-promo'))return; panel.setAttribute('data-aicmp-promo','1'); var PL=''; var AJAX=''; var NONCE=''; var DETAILS='aicmp_promo_details'; var DONE='aicmp_promo_sent_'+PL; function el(sfx){ return document.getElementById(uid+sfx); } var msg=el('_msg'), hint=el('_hint'), send=el('_send'); /* ---- tabs ---- */ var tabs=[el('_t1'),el('_t2')], panes=[el('_p1'),el('_p2')]; function show(n){ tabs.forEach(function(t,i){ if(t) t.setAttribute('aria-selected', i===n?'true':'false'); }); panes.forEach(function(p,i){ if(p) p.hidden = (i!==n); }); } tabs.forEach(function(t,i){ if(t) t.addEventListener('click',function(){ show(i); }); }); show(0); /* ---- stars ---- */ var rating=0; var stars=[].slice.call(panel.querySelectorAll('.aicmp-star')); function paint(v){ stars.forEach(function(b,i){ b.classList.toggle('on', i < v); b.setAttribute('aria-checked', (i+1)===v ? 'true' : 'false'); }); } stars.forEach(function(b){ b.addEventListener('click',function(){ rating=parseInt(b.getAttribute('data-v'),10)||0; paint(rating); }); b.addEventListener('keydown',function(e){ if(e.key==='ArrowRight'||e.key==='ArrowUp'){ e.preventDefault(); rating=Math.min(5,rating+1); paint(rating); stars[rating-1].focus(); } if(e.key==='ArrowLeft'||e.key==='ArrowDown'){ e.preventDefault(); rating=Math.max(1,rating-1); paint(rating); stars[rating-1].focus(); } }); }); /* ---- details, remembered in this browser only ---- */ var fields={ name:el('_nm'), country:el('_co'), organisation:el('_or'), website:el('_ws') }; function loadDetails(){ var raw=null; try{ raw=localStorage.getItem(DETAILS); }catch(e){} if(!raw)return; var d; try{ d=JSON.parse(raw); }catch(e){ return; } Object.keys(fields).forEach(function(k){ if(fields[k]&&d[k]) fields[k].value=d[k]; }); } function saveDetails(){ var d={}; Object.keys(fields).forEach(function(k){ d[k]=fields[k]?fields[k].value.trim():''; }); try{ localStorage.setItem(DETAILS, JSON.stringify(d)); }catch(e){} return d; } Object.keys(fields).forEach(function(k){ if(fields[k]) fields[k].addEventListener('change', saveDetails); }); loadDetails(); /* ---- the lock ---- */ function unlock(){ wrap.classList.add('aicmp-open'); if(hint) hint.textContent=''; if(send){ send.disabled=true; send.textContent=''; } } var already=false; try{ already = sessionStorage.getItem(DONE)==='1' || localStorage.getItem(DONE)==='1'; }catch(e){} if(already) unlock(); /* A locked download should not be clickable at all, not merely faded. */ wrap.addEventListener('click', function(e){ if(wrap.classList.contains('aicmp-open')) return; var a = e.target.closest ? e.target.closest('.aicmp-dl') : null; if(!a) return; e.preventDefault(); e.stopPropagation(); show(0); if(msg){ msg.className='aicmp-msg err'; msg.textContent=''; } panel.scrollIntoView({behavior:'smooth', block:'nearest'}); }, true); /* ---- submit ---- */ if(send) send.addEventListener('click', function(){ var d=saveDetails(); var body=new URLSearchParams(); body.set('action','aicmp_feedback'); body.set('nonce',NONCE); body.set('playlist',PL); body.set('fav', el('_fav') ? el('_fav').value : '0'); body.set('rating', String(rating)); body.set('comments', el('_cmt') ? el('_cmt').value : ''); body.set('name', d.name||''); body.set('country', d.country||''); body.set('organisation', d.organisation||''); body.set('website', d.website||''); send.disabled=true; if(msg){ msg.className='aicmp-msg'; msg.textContent=''; } fetch(AJAX,{method:'POST',credentials:'same-origin',body:body}) .then(function(r){ return r.json().then(function(j){ return {ok:r.ok,j:j}; }); }) .then(function(res){ var ok = res.ok && res.j && res.j.success; if(ok){ try{ localStorage.setItem(DONE,'1'); }catch(e){} if(msg){ msg.className='aicmp-msg ok'; msg.textContent=(res.j.data&&res.j.data.message)||''; } unlock(); }else{ send.disabled=false; var m=(res.j&&res.j.data&&res.j.data.message)||''; if(msg){ msg.className='aicmp-msg err'; msg.textContent=m; } if(/name/i.test(m)) show(1); } }) .catch(function(){ send.disabled=false; if(msg){ msg.className='aicmp-msg err'; msg.textContent=''; } }); }); })();

$t): ?>
⠇
🎵
—
🎵

1 /
0:00 / 0:00
VOL
SENS
DL
(function(){ var d=document.getElementById(''); if(!d)return; /* Dismissed for this session: take the bar out of the document entirely. Hiding it is not enough - autoplay would still start a player nobody can see, hear the end of, or stop. */ /* The launcher is the way back from a dismissed bar. It is defined once and used both by the close button and by later page loads, so the corner button appears the moment the bar is dismissed rather than after a refresh. */ if(!window.aicmpDockLauncher){ window.aicmpDockLauncher=function(){ if(document.querySelector('.aicmp-relaunch'))return; var b=document.createElement('button'); b.type='button'; b.className='aicmp-relaunch'; b.setAttribute('aria-label',''); b.title=''; b.innerHTML=''; b.addEventListener('click',function(){ try{ sessionStorage.removeItem('aicmp_dock_closed'); }catch(e){} if(b.parentNode)b.parentNode.removeChild(b); /* Same page: the bar was only detached, so put it straight back with whatever it was playing. Any other page has to build it fresh. */ if(window.aicmpDockStash){ var back=window.aicmpDockStash; document.body.appendChild(back); window.aicmpDockStash=null; /* This click is the user gesture browsers require, so picking the track back up here is always allowed. */ if(window.aicmpDockWasPlaying){ window.aicmpDockWasPlaying=false; var pb=back.querySelector('.aicmp2-play'); if(pb)pb.click(); } }else{ location.reload(); } }); document.body.appendChild(b); }; } var gone=false; try{ gone=sessionStorage.getItem('aicmp_dock_closed')==='1'; }catch(e){} if(gone){ if(d.parentNode)d.parentNode.removeChild(d); window.aicmpDockLauncher(); return; } var prev=document.querySelector('.aicmp2-dock[data-aicmp-live="1"]'); if(prev&&prev!==d){ var pa=prev.querySelector('audio'); /* a dock that is mid-track wins: drop the newly arrived one instead */ if(pa&&!pa.paused){ if(d.parentNode)d.parentNode.removeChild(d); return; } if(prev.parentNode)prev.parentNode.removeChild(prev); } /* out of the content and onto : keeps position:fixed working under themes that transform a wrapper, and lets the bar survive an AJAX content swap */ if(d.parentNode!==document.body)document.body.appendChild(d); d.setAttribute('data-aicmp-live','1'); })(); (function(){ var uid=''; var lb=document.getElementById(uid+'_lb'),lbimg=document.getElementById(uid+'_lbimg'),dock=document.getElementById(uid); if(lb&&lbimg&&dock){ var closeLb=function(){lb.classList.remove('open');lb.setAttribute('aria-hidden','true');}; dock.addEventListener('click',function(e){ var a=e.target.closest('#'+uid+'_art'); if(!a)return; if(lb.classList.contains('open')){closeLb();return;} var src=(a.tagName==='IMG')?(a.currentSrc||a.src):''; if(!src)return; lbimg.src=src;lb.classList.add('open');lb.setAttribute('aria-hidden','false'); }); lb.addEventListener('click',closeLb); document.addEventListener('keydown',function(e){if(e.key==='Escape')closeLb();}); } })(); (function(){ var uid='',accent='',peakCol=''; var urls=; if(!document.getElementById(uid))return; var tog=document.getElementById(uid+'_vistog'),wrap=document.getElementById(uid+'_viswrap'),cv=document.getElementById(uid+'_viscv'),sens=document.getElementById(uid+'_vissens'),senswrap=document.getElementById(uid+'_senswrap'),au=document.getElementById(uid+'_au'),plw=document.getElementById(uid+'_listwrap'),plt=document.getElementById(uid+'_pltog'); if(!tog||!wrap||!cv||!au)return; function sameOrigin(u){try{return new URL(u,location.href).origin===location.origin;}catch(e){return false;}} if(!urls.length||!urls.every(sameOrigin)){tog.style.display='none';return;} var ctx=cv.getContext('2d'),actx,analyser,srcNode,freq,raf=null,built=false,peaks=[],NB=120,grad=null,binMap=null,gainMap=null,bLo=null,bHi=null,rawv=[]; function build(){ if(built)return true; try{ var AC=window.AudioContext||window.webkitAudioContext; actx=new AC();srcNode=actx.createMediaElementSource(au);analyser=actx.createAnalyser(); analyser.fftSize=4096;analyser.smoothingTimeConstant=0.55;analyser.minDecibels=-80;analyser.maxDecibels=-12; srcNode.connect(analyser);analyser.connect(actx.destination); freq=new Uint8Array(analyser.frequencyBinCount);built=true; }catch(e){built=false;tog.style.display='none';} return built; } function resize(){var r=cv.getBoundingClientRect(),d=window.devicePixelRatio||1;cv.width=Math.max(1,r.width*d);cv.height=Math.max(1,r.height*d);NB=Math.max(48,Math.min(240,Math.round(r.width/5))); if(freq){var bins=freq.length,nyq=(actx?actx.sampleRate:44100)/2,minF=20,maxF=Math.min(18000,nyq),binHz=nyq/bins,ratio=maxF/minF,k,f,bp,g,pivot=200,tilt=0.34,flo,fhi,b0,b1;binMap=new Array(NB);gainMap=new Array(NB);bLo=new Array(NB);bHi=new Array(NB);for(k=0;k1?k/(NB-1):0);bp=f/binHz;if(bp<1)bp=1;if(bp>bins-1)bp=bins-1;binMap[k]=bp;g=f>pivot?Math.pow(f/pivot,tilt):1;if(g>5)g=5;gainMap[k]=g;flo=minF*Math.pow(ratio,NB>1?(k-0.5)/(NB-1):0);fhi=minF*Math.pow(ratio,NB>1?(k+0.5)/(NB-1):0);b0=Math.floor(flo/binHz);b1=Math.floor(fhi/binHz);if(b0<1)b0=1;if(b0>bins-1)b0=bins-1;if(b1bins-1)b1=bins-1;bLo[k]=b0;bHi[k]=b1;}}} function render(){ raf=requestAnimationFrame(render); if(!analyser)return; analyser.getByteFrequencyData(freq); var w=cv.width,h=cv.height,d=window.devicePixelRatio||1,s=parseFloat(sens.value)||1.0; ctx.clearRect(0,0,w,h); var bw=w/NB,bwi=bw*0.72,i,v,bh,p,lo,hi,fr,m,b,sum; for(i=0;ibLo[i]){sum=0;for(b=bLo[i];b<=bHi[i];b++)sum+=freq[b];m=sum/(bHi[i]-bLo[i]+1);} else{lo=binMap?binMap[i]|0:i;hi=(lo+10?rawv[i-1]:rawv[i])*0.24+(i1?rawv[i-2]:rawv[i])*0.06+(i0.85){v=0.85+(1-Math.exp(-(v-0.85)*2.2))*0.15;} if(v>1)v=1; bh=v*h*0.98; ctx.fillRect(i*bw+bw*0.14,h-bh,bwi,bh); p=peaks[i]||0;peaks[i]=bh>p?bh:Math.max(0,p-h*0.009); } ctx.fillStyle=peakCol; for(i=0;i2)ctx.fillRect(i*bw+bw*0.14,h-p-1.5*d,bwi,1.5*d);} } function openVis(){ if(plw){plw.classList.add('aicmp-collapsed');if(plt){plt.classList.remove('open');plt.setAttribute('aria-expanded','false');}} if(!build())return; if(actx.state==='suspended')actx.resume(); wrap.classList.remove('aicmp-collapsed');tog.classList.add('active'); if(senswrap)senswrap.classList.add('aicmp-on'); resize();if(!raf)render(); } function closeVis(){wrap.classList.add('aicmp-collapsed');tog.classList.remove('active');if(senswrap)senswrap.classList.remove('aicmp-on');if(raf){cancelAnimationFrame(raf);raf=null;}} tog.addEventListener('click',function(){wrap.classList.contains('aicmp-collapsed')?openVis():closeVis();}); if(plt){plt.addEventListener('click',function(){if(plw&&!plw.classList.contains('aicmp-collapsed'))closeVis();});} window.addEventListener('resize',function(){if(!wrap.classList.contains('aicmp-collapsed'))resize();}); })(); (function(){ var uid=''; var root=document.getElementById(uid); if(!root||root.getAttribute('data-aicmp-ready'))return; root.setAttribute('data-aicmp-ready','1'); var tracks=; var total=tracks.length,ci=0,playing=false,repeatMode=0; var accent=''; var PLAY='',PAUSE=''; var au=document.getElementById(uid+'_au'); var btn=document.getElementById(uid+'_btn'),ic=document.getElementById(uid+'_ic'); var prevBtn=document.getElementById(uid+'_prev'),nextBtn=document.getElementById(uid+'_next'); var repBtn=document.getElementById(uid+'_rep'); var repOff=document.getElementById(uid+'_rep_off'),repOne=document.getElementById(uid+'_rep_one'),repAll=document.getElementById(uid+'_rep_all'); var cv=document.getElementById(uid+'_cv'),c=cv.getContext('2d'),ww=cv.parentElement; var vl=document.getElementById(uid+'_vl'); var curE=document.getElementById(uid+'_cur'),durE=document.getElementById(uid+'_dur'); var idxE=document.getElementById(uid+'_idx'); var ttlE=document.getElementById(uid+'_ttl'),subE=document.getElementById(uid+'_sub'); var artE=document.getElementById(uid+'_art'); var buyE=document.getElementById(uid+'_buy'),dlE=document.getElementById(uid+'_dl'); var aicmpStatUrl=''; var aicmpP=false,aicmpC=false,aicmpCurId=(tracks[0]&&tracks[0].id)||0; function aicmpStat(type){if(!aicmpCurId)return;try{fetch(aicmpStatUrl,{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({track_id:aicmpCurId,type:type})});}catch(e){}} if(dlE){dlE.addEventListener('click',function(){aicmpStat('download');});} var shufBtn=document.getElementById(uid+'_shuf');var shuffle=false; function randIdx(){if(total<2)return ci;var r;do{r=Math.floor(Math.random()*total);}while(r===ci);return r;} var listEl=document.getElementById(uid+'_list'); var plTog=document.getElementById(uid+'_pltog'),listWrap=document.getElementById(uid+'_listwrap'); if(plTog&&listWrap){plTog.addEventListener('click',function(){var collapsed=listWrap.classList.toggle('aicmp-collapsed');plTog.classList.toggle('open',!collapsed);plTog.setAttribute('aria-expanded',collapsed?'false':'true');plTog.title=collapsed?'Show playlist':'Hide playlist';});} function fmt(s){if(!s||isNaN(s))return'0:00';var m=Math.floor(s/60),sec=Math.floor(s%60);return m+':'+(sec<10?'0':'')+sec;} function draw(p){aicmp_draw_(cv,c,ww,p,accent);} function updateNav(){prevBtn.disabled=ci===0;nextBtn.disabled=shuffle?total<2:ci===total-1;if(idxE)idxE.textContent=(ci+1)+' / '+total;} function updateRepeat(){ repOff.style.display=repeatMode===0?'block':'none'; repOne.style.display=repeatMode===1?'block':'none'; repAll.style.display=repeatMode===2?'block':'none'; repBtn.classList.toggle('active',repeatMode>0); repBtn.title=['Repeat: Off','Repeat: One','Repeat: All'][repeatMode]; } function aicmpMS(){ if(!('mediaSession' in navigator))return; var t=tracks[ci]; try{navigator.mediaSession.metadata=new MediaMetadata({title:t.title||'',artist:t.artist||'',artwork:t.artwork?[{src:t.artwork,sizes:'512x512'}]:[]});}catch(e){} } function aicmpMSpos(){ if(!('mediaSession' in navigator)||!navigator.mediaSession.setPositionState||!au.duration||isNaN(au.duration))return; try{navigator.mediaSession.setPositionState({duration:au.duration,position:Math.min(au.currentTime,au.duration),playbackRate:au.playbackRate||1});}catch(e){} } if('mediaSession' in navigator){var ms=navigator.mediaSession;try{ ms.setActionHandler('play',function(){au.play().catch(function(){});}); ms.setActionHandler('pause',function(){au.pause();}); ms.setActionHandler('previoustrack',function(){if(au.currentTime>3){au.currentTime=0;}else if(ci>0){load(ci-1,true);}else{au.currentTime=0;}}); ms.setActionHandler('nexttrack',function(){if(shuffle){load(randIdx(),true);}else if(ci();draw(0);playing=false;ic.innerHTML=PLAY; curE.textContent='0:00';durE.textContent='0:00'; updateNav(); aicmpMS(); listEl.querySelectorAll('.aicmp-pl-item').forEach(function(el){el.classList.toggle('active',parseInt(el.dataset.i)===i);}); aicmp_analyse_(t.mp3,t.title,function(){draw(au.duration?au.currentTime/au.duration:0);}); if(play){au.play().then(function(){playing=true;ic.innerHTML=PAUSE;}).catch(function(){});} } var dragIdx=null; function initDrag(){ listEl.querySelectorAll('.aicmp-pl-item').forEach(function(el){ el.addEventListener('dragstart',function(e){dragIdx=parseInt(el.dataset.i);el.classList.add('dragging');e.dataTransfer.effectAllowed='move';}); el.addEventListener('dragend',function(){el.classList.remove('dragging');listEl.querySelectorAll('.aicmp-pl-item').forEach(function(x){x.classList.remove('drag-over');});}); el.addEventListener('dragover',function(e){e.preventDefault();listEl.querySelectorAll('.aicmp-pl-item').forEach(function(x){x.classList.remove('drag-over');});el.classList.add('drag-over');}); el.addEventListener('drop',function(e){ e.preventDefault();var dropIdx=parseInt(el.dataset.i); if(dragIdx===null||dragIdx===dropIdx)return; var moved=tracks.splice(dragIdx,1)[0];tracks.splice(dropIdx,0,moved);total=tracks.length; if(ci===dragIdx)ci=dropIdx;else if(dragIdx=ci)ci--;else if(dragIdx>ci&&dropIdx<=ci)ci++; rebuildList();updateNav();dragIdx=null; }); el.addEventListener('click',function(){if(!el.classList.contains('dragging'))load(parseInt(el.dataset.i),true);}); }); } var aicmpIsPromo=root.classList.contains('aicmp-promo'); function aicmpEsc(v){return String(v==null?'':v).replace(/[&<>"]/g,function(ch){return {'&':'&','<':'<','>':'>','"':'"'}[ch];});} function aicmpRowDl(t){ if(!aicmpIsPromo||!t.mp3)return ''; var ext=(t.mp3.split('?')[0].split('.').pop()||'mp3'); return '' +''; } function rebuildList(){ listEl.innerHTML=''; tracks.forEach(function(t,i){ var div=document.createElement('div'); div.className='aicmp-pl-item'+(i===ci?' active':'');div.dataset.i=i;div.draggable=true; var art=t.artwork?'':'
🎵
'; div.innerHTML='⠇'+(i+1)+''+art+'
'+aicmpEsc(t.title)+'
'+aicmpEsc(t.artist)+'
—'+aicmpRowDl(t); listEl.appendChild(div); }); initDrag(); } /* A download inside a row must not also select the track. */ if(aicmpIsPromo&&listEl){ listEl.addEventListener('click',function(e){ var a=e.target.closest?e.target.closest('.aicmp-pl-dl'):null; if(!a)return; e.stopPropagation(); aicmpStat('download'); },true); } aicmp_placeholder_();draw(0);updateNav();updateRepeat();aicmpMS(); (function(){var wrap=document.getElementById(uid);var run=function(){aicmp_analyse_(tracks[0].mp3,tracks[0].title,function(){draw(0);});}; if(wrap&&'IntersectionObserver' in window){var io=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){io.disconnect();run();}});},{rootMargin:'200px'});io.observe(wrap);}else{run();}})(); au.addEventListener('loadedmetadata',function(){durE.textContent=fmt(au.duration);aicmpMSpos();}); au.addEventListener('play',function(){playing=true;ic.innerHTML=PAUSE;if('mediaSession' in navigator)navigator.mediaSession.playbackState='playing';}); au.addEventListener('pause',function(){playing=false;ic.innerHTML=PLAY;if('mediaSession' in navigator)navigator.mediaSession.playbackState='paused';}); au.addEventListener('timeupdate',function(){draw(au.duration?au.currentTime/au.duration:0);curE.textContent=fmt(au.currentTime);aicmpMSpos();if(!aicmpP&&au.currentTime>=5){aicmpP=true;aicmpStat('play');}if(!aicmpC&&au.duration&&au.currentTime/au.duration>=0.8){aicmpC=true;aicmpStat('complete');}}); au.addEventListener('ended',function(){ if(repeatMode===1){au.currentTime=0;au.play();} else if(shuffle){load(randIdx(),true);} else if(repeatMode===2){if(ci0)load(ci-1,playing);}); nextBtn.addEventListener('click',function(){if(shuffle){load(randIdx(),playing);}else if(ci if(!window.aicmpAP){window.aicmpAP=true; au.muted=true; au.play().then(function(){playing=true;ic.innerHTML=PAUSE;}).catch(function(){}); var aicmpUnmute=function(){au.muted=false;}; ['pointerdown','keydown','touchstart'].forEach(function(ev){window.addEventListener(ev,aicmpUnmute,{once:true,passive:true});}); var aicmpApDone=false; var aicmpApGo=function(){if(aicmpApDone)return;aicmpApDone=true;if(playing)return;au.play().then(function(){playing=true;ic.innerHTML=PAUSE;}).catch(function(){});}; ['pointerdown','keydown','touchstart','wheel','scroll'].forEach(function(ev){window.addEventListener(ev,aicmpApGo,{once:true,passive:true});}); } initDrag(); window.addEventListener('resize',function(){draw(au.duration?au.currentTime/au.duration:0);}); if(window.aicmpKnobAll)window.aicmpKnobAll(document.getElementById('')); /* Dismissing the bar. A visitor who does not want music should be able to say so once and be left alone for the rest of the session, rather than on every page. The bar is removed rather than hidden, so nothing is left running behind it. */ if(root.classList.contains('aicmp2-dock')){ var closeBtn=document.getElementById(uid+'_close'); if(closeBtn){ closeBtn.addEventListener('click',function(){ /* Go through the play button rather than pausing the audio directly, so the bar's own state and icon stay in step and it can be resumed the same way. */ window.aicmpDockWasPlaying=playing; if(playing&&btn)btn.click(); try{ au.pause(); }catch(e){} try{ sessionStorage.setItem('aicmp_dock_closed','1'); }catch(e){} if(window.aicmpResume)window.aicmpResume.clear(); /* Detached, not destroyed: a paused player outside the document cannot make a sound, and keeping it means the launcher can restore the track, the position and the volume without a reload. */ if(root.parentNode)root.parentNode.removeChild(root); window.aicmpDockStash=root; if(window.aicmpDockLauncher)window.aicmpDockLauncher(); }); } } /* Continuous playback across pages. Only the docked bar takes part - an inline player belongs to its page and should not follow the visitor around. */ if(window.aicmpResume&&root.classList.contains('aicmp2-dock')){ var R=window.aicmpResume,lastSave=0; var remember=function(){ if(ci<0||ci>=total)return; R.save({i:ci,t:au.currentTime||0,p:playing,v:au.volume,d:tracks[ci].id||0}); }; au.addEventListener('play',remember); au.addEventListener('pause',remember); au.addEventListener('timeupdate',function(){ var now=Date.now(); if(now-lastSave>2000){ lastSave=now; remember(); } }); window.addEventListener('pagehide',remember); var prev=R.read(); if(prev&&typeof prev.i==='number'&&prev.i>=0&&prev.i 0, 'skin' => '1'], $atts); $id = intval($atts['id']); if (!$id) return '

' . esc_html__('AICreativa Music Player: missing playlist ID.', 'aicreativa-music-player') . '

'; $post = get_post($id); if (!$post || $post->post_type !== 'aicmp_playlist') return '

' . esc_html__('AICreativa Music Player: playlist not found.', 'aicreativa-music-player') . '

'; $track_ids = get_post_meta($id, '_aicmp_track_ids', true) ?: []; if (empty($track_ids)) return '

' . esc_html__('No tracks in this playlist.', 'aicreativa-music-player') . '

'; $accent = get_option('aicmp_accent_color', '#73f5fe'); $bg = get_option('aicmp_bg_color', '#1f1e1e'); $text = get_option('aicmp_text_color', '#b3dcea'); $text_soft = aicmp_hex_rgba($text, 0.62); $buy_lbl = get_option('aicmp_buy_label', 'BUY'); $width = get_option('aicmp_player_width', 952); $show_dl = get_option('aicmp_show_download', 1); $autoplay = get_option('aicmp_autoplay', 0); $ap_mode = get_option('aicmp_autoplay_mode', 'interaction'); $tracks = []; foreach ($track_ids as $tid) { $t = get_post($tid); if (!$t) continue; $tracks[] = [ 'id' => (int) $tid, 'title' => $t->post_title, 'artist' => get_post_meta($tid, '_aicmp_artist', true), 'mp3' => get_post_meta($tid, '_aicmp_mp3_url', true), 'buy' => get_post_meta($tid, '_aicmp_buy_url', true), 'artwork' => aicmp_get_artwork($tid), ]; } if (empty($tracks)) return '

' . esc_html__('No valid tracks.', 'aicreativa-music-player') . '

'; $uid = 'aicmppl_' . uniqid(); $total = count($tracks); if ((string) $atts['skin'] === '2') { return aicmp_render_playlist_skin2($uid, $tracks, [ 'accent' => $accent, 'control' => $text, 'text' => $text, 'buy_lbl' => $buy_lbl, 'autoplay' => $autoplay, 'ap_mode' => $ap_mode, ]); } // Skin 3 is the Skin 1 playlist with the promo panel underneath: no BUY, // and the download stays locked until the form has been sent. $promo = ((string) $atts['skin'] === '3'); ob_start(); $first = $tracks[0]; ?>
🎵

1 /
0:00 / 0:00
VOL
$t): ?>
⠇
🎵
—