YoutubeLayout.php
741 lines
| 1 | <?php |
| 2 | |
| 3 | namespace EmbedPress\Providers\TemplateLayouts; |
| 4 | |
| 5 | |
| 6 | use Embera\Provider\ProviderAdapter; |
| 7 | use Embera\Provider\ProviderInterface; |
| 8 | use Embera\Url; |
| 9 | use EmbedPress\Includes\Classes\Helper; |
| 10 | |
| 11 | |
| 12 | class YoutubeLayout |
| 13 | { |
| 14 | |
| 15 | |
| 16 | |
| 17 | public static function create_channel_info_layout($channel_info, $channel_url) |
| 18 | { |
| 19 | $title = isset($channel_info['snippet']['title']) ? $channel_info['snippet']['title'] : null; |
| 20 | $handle = isset($channel_info['snippet']['customUrl']) ? $channel_info['snippet']['customUrl'] : null; |
| 21 | $subscriberCount = isset($channel_info['statistics']['subscriberCount']) ? $channel_info['statistics']['subscriberCount'] : null; |
| 22 | $videoCount = isset($channel_info['statistics']['videoCount']) ? $channel_info['statistics']['videoCount'] : null; |
| 23 | $thumbnailUrl = isset($channel_info['snippet']['thumbnails']['high']['url']) ? $channel_info['snippet']['thumbnails']['high']['url'] : null; |
| 24 | |
| 25 | ob_start(); |
| 26 | ?> |
| 27 | <div class="channel-header" data-channel-url="<?php echo esc_url($channel_url); ?>"> |
| 28 | <img src="<?php echo esc_url($thumbnailUrl); ?>" alt="<?php echo esc_attr($title); ?>" class="profile-picture"> |
| 29 | <div class="channel-info"> |
| 30 | <div class="info-description"> |
| 31 | <h1 class="channel-name"><?php echo esc_html($title); ?></h1> |
| 32 | <p class="channel-details"><?php echo esc_html($handle); ?> · <?php echo esc_html(Helper::format_number($subscriberCount)); ?> subscribers · <?php echo esc_html(Helper::format_number($videoCount)); ?> videos</p> |
| 33 | </div> |
| 34 | <a target="_blank" href="<?php echo esc_url('https://www.youtube.com/' . $handle); ?>" class="subscribe-button"> |
| 35 | <svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 36 | <path fill-rule="evenodd" clip-rule="evenodd" d="M9.467 1.653A1.6 1.6 0 0 1 8.4 3.093 6.026 6.026 0 0 1 14 9.067v4.586c0 1.067.053 1.92.96 2.4a.694.694 0 0 1-.267 1.28H8.88a1.333 1.333 0 1 1-1.76 0H1.36a.693.693 0 0 1-.32-1.28c.907-.48.96-1.333.96-2.4V9.067a6.027 6.027 0 0 1 5.6-5.974 1.493 1.493 0 1 1 1.867-1.44m-1.44 2.774a4.8 4.8 0 0 0-4.694 4.64v4.693c0 .587 0 1.493-.373 2.293h10.133c-.426-.8-.373-1.653-.373-2.293V9.067a4.8 4.8 0 0 0-1.405-3.289c-.874-.874-2.052-1.324-3.288-1.351" fill="#fff" /></svg> |
| 37 | <?php echo esc_html__('Subscribe', 'embedpress'); ?></a> |
| 38 | </div> |
| 39 | </div> |
| 40 | <?php |
| 41 | |
| 42 | $channel_layout = ob_get_clean(); |
| 43 | |
| 44 | return $channel_layout; |
| 45 | } |
| 46 | |
| 47 | |
| 48 | public static function generate_youtube_video_description($video_data) { |
| 49 | $title = isset($video_data['snippet']['title']) ? $video_data['snippet']['title'] : null; |
| 50 | $description = isset($video_data['snippet']['description']) ? $video_data['snippet']['description'] : null; |
| 51 | $publishedAt = isset($video_data['snippet']['publishedAt']) ? date("M j, Y", strtotime($video_data['snippet']['publishedAt'])) : null; |
| 52 | $channelTitle = isset($video_data['snippet']['channelTitle']) ? $video_data['snippet']['channelTitle'] : null; |
| 53 | $viewCount = isset($video_data['statistics']['viewCount']) ? $video_data['statistics']['viewCount'] : null; |
| 54 | $likeCount = isset($video_data['statistics']['likeCount']) ? $video_data['statistics']['likeCount'] : null; |
| 55 | $commentCount = isset($video_data['statistics']['commentCount']) ? $video_data['statistics']['commentCount'] : null; |
| 56 | $videoId = isset($video_data['id']) ? $video_data['id'] : null; |
| 57 | $videoUrl = $videoId ? "https://www.youtube.com/watch?v={$videoId}" : null; |
| 58 | |
| 59 | |
| 60 | $title = esc_html($title); |
| 61 | $description = esc_html($description); |
| 62 | $viewCount = esc_html($viewCount); |
| 63 | $likeCount = esc_html($likeCount); |
| 64 | $commentCount = esc_html($commentCount); |
| 65 | $publishedAt = esc_html($publishedAt); |
| 66 | |
| 67 | $html = " |
| 68 | <div class='youtube-video-description'> |
| 69 | <div class='youtube-video-header'> |
| 70 | <h1>{$title}</h1> |
| 71 | <div class='youtube-video-meta'> |
| 72 | <span>{$viewCount} views</span> |
| 73 | <span>{$publishedAt}</span> |
| 74 | </div> |
| 75 | </div> |
| 76 | <div class='youtube-video-body'> |
| 77 | <p>{$description}</p> |
| 78 | </div> |
| 79 | <div class='youtube-video-footer'> |
| 80 | <div class='youtube-video-stats'> |
| 81 | <span><svg width='20' height='17.539' viewBox='0 0 20 17.539' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M12.648.109a2.814 2.814 0 0 0-3.477 1.93l-.223.781a3.4 3.4 0 0 1-.762 1.367L6.182 6.39a.937.937 0 0 0 1.387 1.261l2.004-2.203a5.3 5.3 0 0 0 1.176-2.113l.223-.781a.937.937 0 1 1 1.805.515l-.223.781a7.2 7.2 0 0 1-1.039 2.168c-.203.285-.227.66-.066.973s.48.508.832.508H17.5a.627.627 0 0 1 .219 1.211.94.94 0 0 0-.375 1.5.624.624 0 0 1-.352 1.027.938.938 0 0 0-.562 1.504.625.625 0 0 1-.265.969.94.94 0 0 0-.563 1.125.627.627 0 0 1-.602.793h-3.809c-.492 0-.977-.145-1.387-.418l-2.41-1.605c-.43-.289-1.012-.172-1.301.262s-.172 1.012.262 1.301l2.41 1.605c.719.48 1.563.734 2.426.734H15a2.5 2.5 0 0 0 2.5-2.422 2.5 2.5 0 0 0 .887-2.461 2.49 2.49 0 0 0 .879-2.722A2.53 2.53 0 0 0 20 8.125a2.5 2.5 0 0 0-2.5-2.5h-3.605c.184-.406.34-.828.461-1.258l.223-.781a2.814 2.814 0 0 0-1.93-3.477M1.25 6.25A1.25 1.25 0 0 0 0 7.5v8.75c0 .691.559 1.25 1.25 1.25h2.5A1.25 1.25 0 0 0 5 16.25V7.5a1.25 1.25 0 0 0-1.25-1.25z' fill='#fff'/></svg> {$likeCount}</span> |
| 82 | <span><svg width='20' height='15.975' viewBox='0 0 20 15.975' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M2.783 9.644a1.5 1.5 0 0 0-.234-1.741c-.664-.699-1.02-1.538-1.02-2.412 0-1.981 1.991-3.994 4.992-3.994s4.992 2.012 4.992 3.994-1.991 3.994-4.992 3.994a6.3 6.3 0 0 1-1.179-.112 1.5 1.5 0 0 0-.958.131q-.192.098-.393.187c-.499.225-1.027.421-1.557.562a14 14 0 0 0 .346-.608zM.031 5.491c0 1.304.537 2.499 1.432 3.441l-.087.159C1.055 9.665.68 10.23.234 10.717a.751.751 0 0 0 .546 1.264c1.342 0 2.699-.415 3.828-.927q.227-.104.443-.212c.471.094.964.14 1.47.14 3.585 0 6.49-2.459 6.49-5.491S10.106 0 6.521 0 .031 2.459.031 5.491m13.479 9.485c.505 0 .995-.05 1.47-.14q.217.109.443.212c1.129.512 2.487.927 3.828.927a.746.746 0 0 0 .542-1.26c-.443-.487-.817-1.051-1.142-1.626l-.087-.159c.899-.945 1.435-2.14 1.435-3.445 0-2.945-2.743-5.351-6.184-5.485q.192.712.193 1.491v.019c2.721.209 4.493 2.106 4.493 3.975 0 .874-.356 1.713-1.02 2.409a1.5 1.5 0 0 0-.234 1.741l.1.184q.118.21.246.424a10 10 0 0 1-1.557-.562q-.201-.089-.393-.187a1.5 1.5 0 0 0-.958-.131q-.566.112-1.179.112c-1.925 0-3.432-.827-4.268-1.944a8.7 8.7 0 0 1-1.56.368c1.058 1.823 3.274 3.078 5.832 3.078' fill='#fff'/></svg> {$commentCount}</span> |
| 83 | </div> |
| 84 | </div> |
| 85 | </div>"; |
| 86 | |
| 87 | return $html; |
| 88 | } |
| 89 | |
| 90 | |
| 91 | public static function create_list_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb){ |
| 92 | |
| 93 | // $channelTitle = $data['get_channel_info']['snippet']['title']; |
| 94 | // $channelThumb = $data['get_channel_info']['snippet']['thumbnails']['default']['url']; |
| 95 | |
| 96 | foreach ($jsonResult->items as $item) : ?> |
| 97 | <?php |
| 98 | $privacyStatus = isset($item->status->privacyStatus) ? $item->status->privacyStatus : null; |
| 99 | $thumbnail = Helper::get_thumbnail_url($item, $options['thumbnail'], $privacyStatus); |
| 100 | $vid = Helper::get_id($item); |
| 101 | $videoTitle = isset($item->snippet->title) ? $item->snippet->title : ''; |
| 102 | $videoDescription = isset($item->snippet->description) ? $item->snippet->description : ''; |
| 103 | |
| 104 | $publishedAt = isset($item->snippet->publishedAt) ? $item->snippet->publishedAt : ''; |
| 105 | |
| 106 | if (empty($gallobj->first_vid)) { |
| 107 | $gallobj->first_vid = $vid; |
| 108 | } |
| 109 | if ($privacyStatus == 'private' && $options['hideprivate']) { |
| 110 | continue; |
| 111 | } |
| 112 | ?> |
| 113 | <div class="item" data-vid="<?php echo $vid; ?>"> |
| 114 | <div class="thumb" style="background: <?php echo "url({$thumbnail}) no-repeat center"; ?>"> |
| 115 | <div class="play-icon"> |
| 116 | <img src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube.svg'); ?>" alt=""> |
| 117 | </div> |
| 118 | </div> |
| 119 | <div class="body youtube-body-content"> |
| 120 | <div class="description-container"> |
| 121 | <div class="thumbnail"><img src="<?php echo esc_url($channelThumb ); ?>"/></div> |
| 122 | <div class="details"> |
| 123 | <div class="channel"><?php echo esc_html($channelTitle); ?></div> |
| 124 | <div class="title"><?php echo esc_html($videoTitle); ?></div> |
| 125 | <p class="description"><?php echo esc_html(Helper::trimTitle($videoDescription, 25)); ?></p> |
| 126 | |
| 127 | <div class="views-time"> |
| 128 | <span class="time"><?php echo esc_html(Helper::timeAgo($publishedAt)); ?></span> |
| 129 | </div> |
| 130 | </div> |
| 131 | </div> |
| 132 | </div> |
| 133 | </div> |
| 134 | <?php endforeach; |
| 135 | } |
| 136 | public static function create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb){ |
| 137 | |
| 138 | // $channelTitle = $data['get_channel_info']['snippet']['title']; |
| 139 | // $channelThumb = $data['get_channel_info']['snippet']['thumbnails']['default']['url']; |
| 140 | |
| 141 | foreach ($jsonResult->items as $item) : ?> |
| 142 | <?php |
| 143 | $privacyStatus = isset($item->status->privacyStatus) ? $item->status->privacyStatus : null; |
| 144 | $thumbnail = Helper::get_thumbnail_url($item, $options['thumbnail'], $privacyStatus); |
| 145 | $vid = Helper::get_id($item); |
| 146 | $videoTitle = isset($item->snippet->title) ? $item->snippet->title : ''; |
| 147 | $publishedAt = isset($item->snippet->publishedAt) ? $item->snippet->publishedAt : ''; |
| 148 | |
| 149 | if (empty($gallobj->first_vid)) { |
| 150 | $gallobj->first_vid = $vid; |
| 151 | } |
| 152 | if ($privacyStatus == 'private' && $options['hideprivate']) { |
| 153 | continue; |
| 154 | } |
| 155 | ?> |
| 156 | <div class="item" data-vid="<?php echo $vid; ?>"> |
| 157 | <div class="thumb" style="background: <?php echo "url({$thumbnail}) no-repeat center"; ?>"> |
| 158 | <div class="play-icon"> |
| 159 | <img src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube.svg'); ?>" alt=""> |
| 160 | </div> |
| 161 | </div> |
| 162 | <div class="body youtube-body-content"> |
| 163 | <div class="description-container"> |
| 164 | <div class="thumbnail"><img src="<?php echo esc_url($channelThumb ); ?>"/></div> |
| 165 | <div class="details"> |
| 166 | <div class="channel"><?php echo esc_html($channelTitle); ?></div> |
| 167 | <div class="title"><?php echo esc_html(Helper::trimTitle($videoTitle, 6)); ?></div> |
| 168 | |
| 169 | <div class="views-time"> |
| 170 | <span class="time"><?php echo esc_html(Helper::timeAgo($publishedAt)); ?></span> |
| 171 | </div> |
| 172 | </div> |
| 173 | </div> |
| 174 | </div> |
| 175 | </div> |
| 176 | <?php endforeach; |
| 177 | } |
| 178 | |
| 179 | |
| 180 | /** |
| 181 | * Build the YouTube playlist "queue" shell (left player, right scrollable |
| 182 | * playlist list, header with title/channel/N-of-M position). This bypasses |
| 183 | * the standard channel/gallery shell — no .ep-youtube__content__block grid, |
| 184 | * no channel-header banner, no per-page pagination strip. Infinite scroll |
| 185 | * is wired client-side against the same playlistItems endpoint. |
| 186 | */ |
| 187 | public static function create_queue_layout($jsonResult, $gallobj, $options, $data, $playlist_info, $first_vid, $next_page_token) { |
| 188 | $playlist_id = isset($options['playlistId']) ? $options['playlistId'] : ''; |
| 189 | $title = !empty($playlist_info['title']) ? $playlist_info['title'] : __('Playlist', 'embedpress'); |
| 190 | $channel_title = !empty($playlist_info['channel_title']) ? $playlist_info['channel_title'] : ''; |
| 191 | $item_count = !empty($playlist_info['item_count']) ? (int) $playlist_info['item_count'] : 0; |
| 192 | $playlist_url = !empty($playlist_info['playlist_url']) ? $playlist_info['playlist_url'] : 'https://www.youtube.com/playlist?list=' . rawurlencode($playlist_id); |
| 193 | $start_vid = !empty($playlist_info['start_vid']) ? $playlist_info['start_vid'] : ''; |
| 194 | |
| 195 | // Deep-link target overrides the playlist's first item. |
| 196 | $opening_vid = $start_vid ?: $first_vid; |
| 197 | |
| 198 | $first_src = $opening_vid |
| 199 | ? 'https://www.youtube.com/embed/' . $opening_vid . '?feature=oembed&enablejsapi=1' |
| 200 | : 'about:blank'; |
| 201 | |
| 202 | // User-configured height drives the whole queue wrapper (player + list), |
| 203 | // not just the iframe. Falls back to the CSS default (480px) when unset. |
| 204 | $wrap_height = !empty($options['maxheight']) ? (int) $options['maxheight'] : 0; |
| 205 | $wrap_style = $wrap_height ? '--epq-h:' . $wrap_height . 'px;' : ''; |
| 206 | |
| 207 | ob_start(); |
| 208 | ?> |
| 209 | <div class="ep-yt-playlist" data-layout="queue" |
| 210 | data-playlist-id="<?php echo esc_attr($playlist_id); ?>" |
| 211 | data-pagesize="<?php echo intval($options['pagesize']); ?>" |
| 212 | data-next-page-token="<?php echo esc_attr($next_page_token); ?>" |
| 213 | data-total="<?php echo esc_attr($item_count); ?>" |
| 214 | <?php if ($wrap_style) : ?>style="<?php echo esc_attr($wrap_style); ?>"<?php endif; ?>> |
| 215 | <div class="ep-yt-queue"> |
| 216 | <div class="ep-yt-queue__player"> |
| 217 | <iframe class="ep-yt-queue__iframe" |
| 218 | src="<?php echo esc_url($first_src); ?>" |
| 219 | frameborder="0" |
| 220 | allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" |
| 221 | allowfullscreen |
| 222 | title="<?php echo esc_attr($title); ?>"></iframe> |
| 223 | </div> |
| 224 | <aside class="ep-yt-queue__list"> |
| 225 | <header class="ep-yt-queue__header"> |
| 226 | <a class="ep-yt-queue__title" href="<?php echo esc_url($playlist_url); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html($title); ?></a> |
| 227 | <div class="ep-yt-queue__meta"> |
| 228 | <span class="ep-yt-queue__channel"><?php echo esc_html($channel_title); ?></span> |
| 229 | <span class="ep-yt-queue__sep">·</span> |
| 230 | <span class="ep-yt-queue__position"><span class="ep-yt-queue__position-current">1</span> / <span class="ep-yt-queue__position-total"><?php echo esc_html($item_count); ?></span></span> |
| 231 | </div> |
| 232 | <div class="ep-yt-queue__controls"> |
| 233 | <button type="button" class="ep-yt-queue__loop ep-yt-icon-btn" aria-pressed="false" aria-label="<?php echo esc_attr__('Loop', 'embedpress'); ?>" title="<?php echo esc_attr__('Loop', 'embedpress'); ?>"> |
| 234 | <span class="ep-yt-icon ep-yt-icon--loop" aria-hidden="true"></span> |
| 235 | </button> |
| 236 | <button type="button" class="ep-yt-queue__shuffle ep-yt-icon-btn" aria-pressed="false" aria-label="<?php echo esc_attr__('Shuffle', 'embedpress'); ?>" title="<?php echo esc_attr__('Shuffle', 'embedpress'); ?>"> |
| 237 | <span class="ep-yt-icon ep-yt-icon--shuffle" aria-hidden="true"></span> |
| 238 | </button> |
| 239 | </div> |
| 240 | </header> |
| 241 | <ol class="ep-yt-queue__items"> |
| 242 | <?php echo self::render_queue_items($jsonResult, $options, 0, $opening_vid); ?> |
| 243 | </ol> |
| 244 | <div class="ep-yt-queue__loader" hidden> |
| 245 | <span class="ep-yt-queue__spinner" aria-hidden="true"></span> |
| 246 | <span class="ep-yt-queue__loader-text"><?php esc_html_e('Loading more…', 'embedpress'); ?></span> |
| 247 | </div> |
| 248 | </aside> |
| 249 | </div> |
| 250 | </div> |
| 251 | <?php |
| 252 | return ob_get_clean(); |
| 253 | } |
| 254 | |
| 255 | /** |
| 256 | * Render a YouTube playlist as a "Theatre" layout — large player on top, |
| 257 | * horizontal scrolling thumbnail strip below with prev/next controls. |
| 258 | * Wrapped in .ep-yt-playlist so it shares the same scoped CSS reset as |
| 259 | * the queue (no theme/plugin style leakage in either direction). |
| 260 | */ |
| 261 | public static function create_theatre_layout($jsonResult, $gallobj, $options, $data, $playlist_info, $first_vid, $next_page_token) { |
| 262 | $playlist_id = isset($options['playlistId']) ? $options['playlistId'] : ''; |
| 263 | $title = !empty($playlist_info['title']) ? $playlist_info['title'] : __('Playlist', 'embedpress'); |
| 264 | $channel_title = !empty($playlist_info['channel_title']) ? $playlist_info['channel_title'] : ''; |
| 265 | $item_count = !empty($playlist_info['item_count']) ? (int) $playlist_info['item_count'] : 0; |
| 266 | $playlist_url = !empty($playlist_info['playlist_url']) ? $playlist_info['playlist_url'] : 'https://www.youtube.com/playlist?list=' . rawurlencode($playlist_id); |
| 267 | $start_vid = !empty($playlist_info['start_vid']) ? $playlist_info['start_vid'] : ''; |
| 268 | $opening_vid = $start_vid ?: $first_vid; |
| 269 | $first_src = $opening_vid |
| 270 | ? 'https://www.youtube.com/embed/' . $opening_vid . '?feature=oembed&enablejsapi=1' |
| 271 | : 'about:blank'; |
| 272 | |
| 273 | ob_start(); |
| 274 | ?> |
| 275 | <div class="ep-yt-playlist" data-layout="theatre" |
| 276 | data-playlist-id="<?php echo esc_attr($playlist_id); ?>" |
| 277 | data-pagesize="<?php echo intval($options['pagesize']); ?>" |
| 278 | data-next-page-token="<?php echo esc_attr($next_page_token); ?>" |
| 279 | data-total="<?php echo esc_attr($item_count); ?>"> |
| 280 | <div class="ep-yt-theatre"> |
| 281 | <div class="ep-yt-theatre__player"> |
| 282 | <iframe class="ep-yt-theatre__iframe" |
| 283 | src="<?php echo esc_url($first_src); ?>" |
| 284 | frameborder="0" |
| 285 | allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" |
| 286 | allowfullscreen |
| 287 | title="<?php echo esc_attr($title); ?>"></iframe> |
| 288 | </div> |
| 289 | <header class="ep-yt-theatre__meta"> |
| 290 | <a class="ep-yt-theatre__title" href="<?php echo esc_url($playlist_url); ?>" target="_blank" rel="noopener noreferrer"> |
| 291 | <span class="ep-yt-theatre__playicon" aria-hidden="true">▶</span> |
| 292 | <?php echo esc_html($title); ?> |
| 293 | </a> |
| 294 | <div class="ep-yt-theatre__sub"> |
| 295 | <?php if ($channel_title) : ?> |
| 296 | <span class="ep-yt-theatre__channel"><?php echo esc_html($channel_title); ?></span> |
| 297 | <span class="ep-yt-theatre__sep">·</span> |
| 298 | <?php endif; ?> |
| 299 | <span class="ep-yt-theatre__position"><span class="ep-yt-theatre__position-current">1</span> / <span class="ep-yt-theatre__position-total"><?php echo esc_html($item_count); ?></span></span> |
| 300 | <span class="ep-yt-theatre__sep">·</span> |
| 301 | <div class="ep-yt-theatre__controls"> |
| 302 | <button type="button" class="ep-yt-theatre__loop ep-yt-icon-btn" aria-pressed="false" aria-label="<?php echo esc_attr__('Loop', 'embedpress'); ?>" title="<?php echo esc_attr__('Loop', 'embedpress'); ?>"> |
| 303 | <span class="ep-yt-icon ep-yt-icon--loop" aria-hidden="true"></span> |
| 304 | </button> |
| 305 | <button type="button" class="ep-yt-theatre__shuffle ep-yt-icon-btn" aria-pressed="false" aria-label="<?php echo esc_attr__('Shuffle', 'embedpress'); ?>" title="<?php echo esc_attr__('Shuffle', 'embedpress'); ?>"> |
| 306 | <span class="ep-yt-icon ep-yt-icon--shuffle" aria-hidden="true"></span> |
| 307 | </button> |
| 308 | </div> |
| 309 | </div> |
| 310 | </header> |
| 311 | <div class="ep-yt-theatre__strip-wrap"> |
| 312 | <button type="button" class="ep-yt-theatre__nav ep-yt-theatre__nav--prev ep-yt-icon-btn" aria-label="<?php echo esc_attr__('Scroll left', 'embedpress'); ?>"> |
| 313 | <span class="ep-yt-icon ep-yt-icon--chev-left" aria-hidden="true"></span> |
| 314 | </button> |
| 315 | <ol class="ep-yt-theatre__strip"> |
| 316 | <?php echo self::render_theatre_cards($jsonResult, $options, 0, $opening_vid); ?> |
| 317 | </ol> |
| 318 | <button type="button" class="ep-yt-theatre__nav ep-yt-theatre__nav--next ep-yt-icon-btn" aria-label="<?php echo esc_attr__('Scroll right', 'embedpress'); ?>"> |
| 319 | <span class="ep-yt-icon ep-yt-icon--chev-right" aria-hidden="true"></span> |
| 320 | </button> |
| 321 | </div> |
| 322 | <div class="ep-yt-theatre__loader" hidden> |
| 323 | <span class="ep-yt-theatre__spinner" aria-hidden="true"></span> |
| 324 | <span class="ep-yt-theatre__loader-text"><?php esc_html_e('Loading more…', 'embedpress'); ?></span> |
| 325 | </div> |
| 326 | </div> |
| 327 | </div> |
| 328 | <?php |
| 329 | return ob_get_clean(); |
| 330 | } |
| 331 | |
| 332 | /** |
| 333 | * Render a batch of theatre cards. Same contract as render_queue_items |
| 334 | * but in the horizontal card shape — used for both initial render and |
| 335 | * the infinite-scroll REST endpoint. |
| 336 | */ |
| 337 | public static function render_theatre_cards($jsonResult, $options, $offset = 0, $active_vid = '') { |
| 338 | if (empty($jsonResult->items) || !is_array($jsonResult->items)) { |
| 339 | return ''; |
| 340 | } |
| 341 | ob_start(); |
| 342 | $i = (int) $offset; |
| 343 | $auto_active = empty($active_vid) && $offset === 0; |
| 344 | foreach ($jsonResult->items as $item) { |
| 345 | $i++; |
| 346 | $privacyStatus = isset($item->status->privacyStatus) ? $item->status->privacyStatus : null; |
| 347 | if ($privacyStatus === 'private' && !empty($options['hideprivate'])) { |
| 348 | continue; |
| 349 | } |
| 350 | $vid = Helper::get_id($item); |
| 351 | $title = isset($item->snippet->title) ? $item->snippet->title : ''; |
| 352 | $channel = isset($item->snippet->videoOwnerChannelTitle) |
| 353 | ? $item->snippet->videoOwnerChannelTitle |
| 354 | : (isset($item->snippet->channelTitle) ? $item->snippet->channelTitle : ''); |
| 355 | $thumbnail = Helper::get_thumbnail_url($item, !empty($options['thumbnail']) ? $options['thumbnail'] : 'medium', $privacyStatus); |
| 356 | $active = ''; |
| 357 | if ($active_vid && $vid === $active_vid) { |
| 358 | $active = ' is-active'; |
| 359 | } elseif ($auto_active && $i === 1) { |
| 360 | $active = ' is-active'; |
| 361 | } |
| 362 | ?> |
| 363 | <li class="ep-yt-theatre__card<?php echo $active; ?>" data-vid="<?php echo esc_attr($vid); ?>" data-index="<?php echo esc_attr($i); ?>"> |
| 364 | <span class="ep-yt-theatre__card-thumb-wrap"> |
| 365 | <img class="ep-yt-theatre__card-thumb" src="<?php echo esc_url($thumbnail); ?>" alt="" loading="lazy" /> |
| 366 | <span class="ep-yt-theatre__card-num"><?php echo esc_html($i); ?></span> |
| 367 | </span> |
| 368 | <span class="ep-yt-theatre__card-title"><?php echo esc_html($title); ?></span> |
| 369 | <?php if ($channel) : ?> |
| 370 | <span class="ep-yt-theatre__card-channel"><?php echo esc_html($channel); ?></span> |
| 371 | <?php endif; ?> |
| 372 | </li> |
| 373 | <?php |
| 374 | } |
| 375 | return ob_get_clean(); |
| 376 | } |
| 377 | |
| 378 | /** |
| 379 | * Render a batch of queue items. Used both for the initial render and for |
| 380 | * infinite-scroll AJAX response payloads, so the markup contract is |
| 381 | * defined in exactly one place. |
| 382 | */ |
| 383 | public static function render_queue_items($jsonResult, $options, $offset = 0, $active_vid = '') { |
| 384 | if (empty($jsonResult->items) || !is_array($jsonResult->items)) { |
| 385 | return ''; |
| 386 | } |
| 387 | ob_start(); |
| 388 | $i = (int) $offset; |
| 389 | // No explicit active vid → first rendered item is active. |
| 390 | $auto_active = empty($active_vid) && $offset === 0; |
| 391 | foreach ($jsonResult->items as $item) { |
| 392 | $i++; |
| 393 | $privacyStatus = isset($item->status->privacyStatus) ? $item->status->privacyStatus : null; |
| 394 | if ($privacyStatus === 'private' && !empty($options['hideprivate'])) { |
| 395 | continue; |
| 396 | } |
| 397 | $vid = Helper::get_id($item); |
| 398 | $title = isset($item->snippet->title) ? $item->snippet->title : ''; |
| 399 | $channel = isset($item->snippet->videoOwnerChannelTitle) |
| 400 | ? $item->snippet->videoOwnerChannelTitle |
| 401 | : (isset($item->snippet->channelTitle) ? $item->snippet->channelTitle : ''); |
| 402 | $thumbnail = Helper::get_thumbnail_url($item, !empty($options['thumbnail']) ? $options['thumbnail'] : 'medium', $privacyStatus); |
| 403 | $active = ''; |
| 404 | if ($active_vid && $vid === $active_vid) { |
| 405 | $active = ' is-active'; |
| 406 | } elseif ($auto_active && $i === 1) { |
| 407 | $active = ' is-active'; |
| 408 | } |
| 409 | ?> |
| 410 | <li class="ep-yt-queue__item<?php echo $active; ?>" data-vid="<?php echo esc_attr($vid); ?>" data-index="<?php echo esc_attr($i); ?>" data-thumb="<?php echo esc_attr($thumbnail); ?>"> |
| 411 | <span class="ep-yt-queue__item-index"><?php echo esc_html($i); ?></span> |
| 412 | <img class="ep-yt-queue__item-thumb" src="<?php echo esc_url($thumbnail); ?>" alt="" loading="lazy" /> |
| 413 | <span class="ep-yt-queue__item-body"> |
| 414 | <span class="ep-yt-queue__item-title"><?php echo esc_html($title); ?></span> |
| 415 | <?php if ($channel) : ?> |
| 416 | <span class="ep-yt-queue__item-channel"><?php echo esc_html($channel); ?></span> |
| 417 | <?php endif; ?> |
| 418 | </span> |
| 419 | </li> |
| 420 | <?php |
| 421 | } |
| 422 | return ob_get_clean(); |
| 423 | } |
| 424 | |
| 425 | public static function create_youtube_layout($options, $data, $layout, $url) { |
| 426 | $nextPageToken = ''; |
| 427 | $prevPageToken = ''; |
| 428 | $gallobj = new \stdClass(); |
| 429 | $options = wp_parse_args($options, [ |
| 430 | 'playlistId' => '', |
| 431 | 'pageToken' => '', |
| 432 | 'pagesize' => $data['get_pagesize'] ? $data['get_pagesize'] : 6, |
| 433 | 'currentpage' => '', |
| 434 | 'columns' => 3, |
| 435 | 'thumbnail' => 'medium', |
| 436 | 'gallery' => true, |
| 437 | 'autonext' => true, |
| 438 | 'thumbplay' => true, |
| 439 | 'apiKey' => $data['get_api_key'], |
| 440 | 'hideprivate' => '', |
| 441 | ]); |
| 442 | $options['pagesize'] = $options['pagesize'] > 50 ? 50 : $options['pagesize']; |
| 443 | $options['pagesize'] = $options['pagesize'] < 1 ? 1 : $options['pagesize']; |
| 444 | |
| 445 | if (empty($options['apiKey'])) { |
| 446 | $gallobj->html = $data['get_api_key_error_message']; |
| 447 | return $gallobj; |
| 448 | } |
| 449 | |
| 450 | $apiEndpoint = 'https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,status,contentDetails&playlistId=' . $options['playlistId'] |
| 451 | . '&maxResults=' . $options['pagesize'] |
| 452 | . '&key=' . $options['apiKey']; |
| 453 | if ($options['pageToken'] != null) { |
| 454 | $apiEndpoint .= '&pageToken=' . $options['pageToken']; |
| 455 | } |
| 456 | |
| 457 | $transient_key = 'ep_embed_youtube_channel_' . md5($apiEndpoint); |
| 458 | $gallobj->transient_key = $transient_key; |
| 459 | $jsonResult = get_transient($transient_key); |
| 460 | |
| 461 | |
| 462 | if (empty($jsonResult)) { |
| 463 | $apiResult = wp_remote_get($apiEndpoint, array('timeout' => $data['curltimeout'])); |
| 464 | if (is_wp_error($apiResult)) { |
| 465 | $gallobj->html = Helper::clean_api_error_html($apiResult->get_error_message(), true); |
| 466 | return $gallobj; |
| 467 | } |
| 468 | $jsonResult = json_decode($apiResult['body']); |
| 469 | |
| 470 | if (empty($jsonResult->error)) { |
| 471 | set_transient($transient_key, $jsonResult, MINUTE_IN_SECONDS * 20); |
| 472 | } |
| 473 | else{ |
| 474 | set_transient($transient_key, $jsonResult, 10); |
| 475 | } |
| 476 | } |
| 477 | |
| 478 | |
| 479 | if (isset($jsonResult->error)) { |
| 480 | if(!empty($jsonResult->error->errors[0]->reason) && $jsonResult->error->errors[0]->reason == 'playlistNotFound'){ |
| 481 | $gallobj->html = Helper::clean_api_error_html(__('There is nothing on the playlist.', 'embedpress')); |
| 482 | return $gallobj; |
| 483 | } |
| 484 | if (isset($jsonResult->error->message)) { |
| 485 | $gallobj->html = Helper::clean_api_error_html($jsonResult->error->message); |
| 486 | return $gallobj; |
| 487 | } |
| 488 | $gallobj->html = Helper::clean_api_error_html(__('Sorry, there may be an issue with your YouTube API key.', 'embedpress')); |
| 489 | return $gallobj; |
| 490 | } |
| 491 | |
| 492 | |
| 493 | |
| 494 | $resultsPerPage = $jsonResult->pageInfo->resultsPerPage; |
| 495 | $totalResults = $jsonResult->pageInfo->totalResults; |
| 496 | $totalPages = ceil($totalResults / $resultsPerPage); |
| 497 | if (isset($jsonResult->nextPageToken)) { |
| 498 | $nextPageToken = $jsonResult->nextPageToken; |
| 499 | } |
| 500 | |
| 501 | if (isset($jsonResult->prevPageToken)) { |
| 502 | $prevPageToken = $jsonResult->prevPageToken; |
| 503 | } |
| 504 | |
| 505 | |
| 506 | if (!empty($jsonResult->items) && is_array($jsonResult->items)) : |
| 507 | if($options['gallery'] === "false"){ |
| 508 | $gallobj->html = ""; |
| 509 | if(count($jsonResult->items) === 1){ |
| 510 | $gallobj->first_vid = Helper::get_id($jsonResult->items[0]); |
| 511 | } |
| 512 | return $gallobj; |
| 513 | } |
| 514 | |
| 515 | // Playlist-only layouts: own shells, own JS wiring, no |
| 516 | // channel-header / grid wrapper / pagination strip. Each one is |
| 517 | // isolated under .ep-yt-playlist with its own scoped CSS reset |
| 518 | // so theme + other plugin styles can't leak in. |
| 519 | $playlist_layouts = ['queue', 'theatre', 'library', 'spotlight', 'cinema', 'magazine']; |
| 520 | if (in_array($layout, $playlist_layouts, true)) { |
| 521 | $gallobj->first_vid = Helper::get_id($jsonResult->items[0]); |
| 522 | $playlist_info = !empty($data['playlist_info']) ? $data['playlist_info'] : []; |
| 523 | if ($layout === 'theatre') { |
| 524 | $gallobj->html = self::create_theatre_layout($jsonResult, $gallobj, $options, $data, $playlist_info, $gallobj->first_vid, $nextPageToken); |
| 525 | } elseif ($layout === 'queue') { |
| 526 | $gallobj->html = self::create_queue_layout($jsonResult, $gallobj, $options, $data, $playlist_info, $gallobj->first_vid, $nextPageToken); |
| 527 | } else { |
| 528 | // Pro layouts (library, spotlight, cinema, magazine) — Pro |
| 529 | // plugin handlers register on these actions. Action prints |
| 530 | // the HTML into the ob buffer (mirrors channel grid/carousel |
| 531 | // pattern). Free falls back to queue earlier in the chain |
| 532 | // when Pro is inactive, so this only fires for Pro users. |
| 533 | ob_start(); |
| 534 | do_action( |
| 535 | 'embedpress/youtube_' . $layout . '_layout', |
| 536 | $jsonResult, $gallobj, $options, $data, $playlist_info, $gallobj->first_vid, $nextPageToken |
| 537 | ); |
| 538 | $rendered = ob_get_clean(); |
| 539 | // If no handler printed anything, fall back to queue so we |
| 540 | // never ship an empty embed in production. |
| 541 | $gallobj->html = !empty($rendered) |
| 542 | ? $rendered |
| 543 | : self::create_queue_layout($jsonResult, $gallobj, $options, $data, $playlist_info, $gallobj->first_vid, $nextPageToken); |
| 544 | } |
| 545 | return $gallobj; |
| 546 | } |
| 547 | |
| 548 | // if(count($jsonResult->items) === 1 && empty($nextPageToken) && empty($prevPageToken)){ |
| 549 | // $gallobj->first_vid = Helper::get_id($jsonResult->items[0]); |
| 550 | // $gallobj->html = ""; |
| 551 | // return $gallobj; |
| 552 | // } |
| 553 | |
| 554 | if (strpos($options['playlistId'], 'UU') === 0) { |
| 555 | // sort only channels |
| 556 | usort($jsonResult->items, array(Helper::class, 'compare_vid_date')); // sorts in place |
| 557 | } |
| 558 | |
| 559 | |
| 560 | ob_start(); |
| 561 | |
| 562 | ?> |
| 563 | |
| 564 | |
| 565 | <?php |
| 566 | $channel_info = $data['get_channel_info']; |
| 567 | |
| 568 | |
| 569 | |
| 570 | $chanelTitle = isset($channel_info['snippet']['title']) ? $channel_info['snippet']['title'] : null; |
| 571 | $channelThumb = isset($channel_info['snippet']['thumbnails']['high']['url']) ? $channel_info['snippet']['thumbnails']['high']['url'] : null; |
| 572 | |
| 573 | |
| 574 | echo self::create_channel_info_layout($channel_info, $url); |
| 575 | |
| 576 | $channel_id = ''; |
| 577 | if(isset($channel_info['id'])) { |
| 578 | $channel_id = $channel_info['id']; |
| 579 | } |
| 580 | |
| 581 | $carouselWrapperClass = ''; |
| 582 | $carouselSelectorId = ''; |
| 583 | if($layout == 'carousel') { |
| 584 | $carouselWrapperClass = 'youtube-carousel'; |
| 585 | $carouselSelectorId = 'data-youtube-channel-carousel="carousel-'.esc_attr(md5($channel_id)).'"'; |
| 586 | } |
| 587 | |
| 588 | ?> |
| 589 | |
| 590 | <div class="ep-youtube__content__block" <?php echo $carouselSelectorId; ?> data-unique-id="<?php echo esc_attr(md5($channel_id)); ?>"> |
| 591 | <div class="youtube__content__body youtube-carousel-container"> |
| 592 | <div class="content__wrap <?php echo esc_attr($carouselWrapperClass); ?>" > |
| 593 | |
| 594 | <?php |
| 595 | if($layout === 'gallery'){ |
| 596 | echo self::create_gallery_layout($jsonResult, $gallobj, $options, $data, $chanelTitle, $channelThumb); |
| 597 | } |
| 598 | |
| 599 | else if($layout === 'list'){ |
| 600 | echo self::create_list_layout($jsonResult, $gallobj, $options, $data, $chanelTitle, $channelThumb); |
| 601 | } |
| 602 | else if($layout === 'grid'){ |
| 603 | do_action('embedpress/youtube_grid_layout', $jsonResult, $gallobj, $options, $data, $chanelTitle, $channelThumb); |
| 604 | |
| 605 | } |
| 606 | else if($layout === 'carousel'){ |
| 607 | do_action('embedpress/youtube_carousel_layout', $jsonResult, $gallobj, $options, $data, $chanelTitle, $channelThumb); |
| 608 | } |
| 609 | else{ |
| 610 | echo self::create_gallery_layout($jsonResult, $gallobj, $options, $data, $chanelTitle, $channelThumb); |
| 611 | |
| 612 | } |
| 613 | ?> |
| 614 | <div class="item" style="height: 0"></div> |
| 615 | </div> |
| 616 | |
| 617 | <!-- Pagination and other content remains unchanged --> |
| 618 | <?php if ($totalPages > 1 && $layout !== 'carousel') : ?> |
| 619 | <div class="ep-youtube__content__pagination <?php echo (empty($prevPageToken) && empty($nextPageToken)) ? ' hide ' : ''; ?>"> |
| 620 | <div |
| 621 | class="ep-prev" <?php echo empty($prevPageToken) ? ' style="display:none" ' : ''; ?> |
| 622 | data-playlistid="<?php echo esc_attr($options['playlistId']) ?>" |
| 623 | data-pagetoken="<?php echo esc_attr($prevPageToken) ?>" |
| 624 | data-pagesize="<?php echo intval($options['pagesize']) ?>" |
| 625 | |
| 626 | > |
| 627 | <span><?php _e("Prev", "embedpress"); ?></span> |
| 628 | </div> |
| 629 | <div class="is_desktop_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>"> |
| 630 | <?php |
| 631 | |
| 632 | $numOfPages = $totalPages; |
| 633 | $renderedEllipses = false; |
| 634 | |
| 635 | $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1; |
| 636 | |
| 637 | for($i = 1; $i<=$numOfPages; $i++) |
| 638 | { |
| 639 | //render pages 1 - 3 |
| 640 | if($i < 4) { |
| 641 | //render link |
| 642 | $is_current = $i == (int)$currentPage? "active__current_page" : ""; |
| 643 | |
| 644 | echo wp_kses_post("<span class='page-number $is_current' data-page='$i'>$i</span>"); |
| 645 | |
| 646 | } |
| 647 | |
| 648 | //render current page number |
| 649 | else if($i == (int)$currentPage) { |
| 650 | //render link |
| 651 | echo wp_kses_post('<span class="page-number active__current_page" data-page="'.$i.'">'.$i.'</span>'); |
| 652 | //reset ellipses |
| 653 | $renderedEllipses = false; |
| 654 | } |
| 655 | |
| 656 | //last page number |
| 657 | else if ($i >= $numOfPages - 1) { |
| 658 | //render link |
| 659 | echo wp_kses_post('<span class="page-number" data-page="'.$i.'">'.$i.'</span>'); |
| 660 | } |
| 661 | |
| 662 | //make sure you only do this once per ellipses group |
| 663 | else { |
| 664 | if (!$renderedEllipses){ |
| 665 | print("..."); |
| 666 | $renderedEllipses = true; |
| 667 | } |
| 668 | } |
| 669 | } |
| 670 | ?> |
| 671 | |
| 672 | </div> |
| 673 | |
| 674 | <div class="is_mobile_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>"> |
| 675 | <?php |
| 676 | |
| 677 | $numOfPages = $totalPages; |
| 678 | $renderedEllipses = false; |
| 679 | |
| 680 | $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1; |
| 681 | |
| 682 | for($i = 1; $i<=$numOfPages; $i++) |
| 683 | { |
| 684 | |
| 685 | //render current page number |
| 686 | if($i == (int)$currentPage) { |
| 687 | //render link |
| 688 | echo wp_kses_post('<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>'); |
| 689 | //reset ellipses |
| 690 | $renderedEllipses = false; |
| 691 | } |
| 692 | |
| 693 | //last page number |
| 694 | else if ($i >= $numOfPages ) { |
| 695 | //render link |
| 696 | echo wp_kses_post('...<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>'); |
| 697 | } |
| 698 | } |
| 699 | ?> |
| 700 | |
| 701 | </div> |
| 702 | |
| 703 | |
| 704 | <div |
| 705 | class="ep-next " <?php echo empty($nextPageToken) ? ' style="display:none" ' : ''; ?> |
| 706 | data-playlistid="<?php echo esc_attr($options['playlistId']) ?>" |
| 707 | data-pagetoken="<?php echo esc_attr($nextPageToken) ?>" |
| 708 | data-pagesize="<?php echo intval($options['pagesize']) ?>" |
| 709 | > |
| 710 | <span><?php _e("Next ", "embedpress"); ?> </span> |
| 711 | </div> |
| 712 | </div> |
| 713 | <?php endif; ?> |
| 714 | |
| 715 | </div> |
| 716 | |
| 717 | |
| 718 | <?php if($layout === 'carousel'): ?> |
| 719 | <div class="carousel-controls"> |
| 720 | <button class="preview" type="button" aria-label="<?php esc_attr_e('Previous', 'embedpress'); ?>"><span aria-hidden="true">❮</span></button> |
| 721 | <button class="next" type="button" aria-label="<?php esc_attr_e('Next', 'embedpress'); ?>"><span aria-hidden="true">❯</span></button> |
| 722 | <?php endif; ?> |
| 723 | |
| 724 | </div> |
| 725 | |
| 726 | <?php |
| 727 | $gallobj->html = ob_get_clean(); |
| 728 | else: |
| 729 | $gallobj->html = Helper::clean_api_error_html(__("There is nothing on the playlist.", 'embedpress')); |
| 730 | endif; |
| 731 | |
| 732 | return $gallobj; |
| 733 | } |
| 734 | |
| 735 | } |
| 736 | |
| 737 | |
| 738 | |
| 739 | |
| 740 | |
| 741 |