PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.75
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.75
9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 8.5.4 All 221 releases
wpvr / admin / helpers / class-wpvr-format.php

class-wpvr-format.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 8.5.75, at admin/helpers/class-wpvr-format.php

2,683 lines 114.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) exit; // Exit if accessed directly
3 /**
4 * Responsible for managing WPVR data formation
5 *
6 * @link http://rextheme.com/
7 * @since 1.0.0
8 *
9 * @package Wpvr
10 * @subpackage Wpvr/admin/views
11 */
12
13 class WPVR_Format
14 {
15
16 /**
17 * Setup checkboxes values for meta fields
18 * @param string $arg
19 *
20 * @return bool
21 * @since 8.0.0
22 */
23 public function set_checkbox_value($arg)
24 {
25 $data = sanitize_text_field($arg);
26 if ($data == 'off') {
27 return $data = false;
28 } else {
29 return $data = true;
30 }
31 }
32
33
34 /**
35 * Setup checkboxes values for meta fields while default value is on
36 * @param string $arg
37 *
38 * @return bool
39 * @since 8.0.0
40 */
41 public function set_checkbox_on_value($arg)
42 {
43 $data = sanitize_text_field($arg);
44 if ($data == 'on') {
45 return $data = true;
46 } else {
47 return $data = false;
48 }
49 }
50
51
52 /**
53 * Setup checkboxes values for pro version meta fields
54 * @param string $arg
55 *
56 * @return bool
57 * @since 8.0.0
58 */
59 public function set_pro_checkbox_value($arg)
60 {
61 $data = sanitize_text_field($arg);
62 if ($data == 'on') {
63 return $data = 'on';
64 } else {
65 return $data = 'off';
66 }
67 }
68
69
70 /**
71 * Preapre default scene ID for Tour
72 * @param array $panodata
73 *
74 * @return string
75 * @since 8.0.0
76 */
77 public function prepare_default_scene($panodata)
78 {
79 $allsceneids = array();
80
81 foreach ($panodata["scene-list"] as $panoscenes) {
82 if (!empty($panoscenes['scene-id'])) {
83 array_push($allsceneids, $panoscenes['scene-id']);
84 }
85 if ($panoscenes['dscene'] == 'on') {
86 return $default_scene = $panoscenes['scene-id'];
87 }
88 }
89
90 if (empty($default_scene)) {
91 if ($allsceneids) {
92 return $default_scene = $allsceneids[0];
93 }
94 }
95 }
96
97
98 /**
99 * Prepare panaromic data for Tour
100 *
101 * @param string $panodata
102 *
103 * @return array
104 * @since 8.0.0
105 */
106 public function prepare_panodata($panodata)
107 {
108 $panolist = stripslashes($panodata);
109 $panodata = (array)json_decode($panolist);
110 $panolist = array();
111 if (is_array($panodata["scene-list"])) {
112 foreach ($panodata["scene-list"] as $scenes_data) {
113 $temp_array = array();
114 $temp_array = (array)$scenes_data;
115
116 if ($temp_array['hotspot-list']) {
117 $_hotspot_array = array();
118 foreach ($temp_array['hotspot-list'] as $temp_hotspot) {
119
120 $temp_hotspot = (array)$temp_hotspot;
121 $_hotspot_array[] = $temp_hotspot;
122 }
123 }
124
125 $temp_array['hotspot-list'] = $_hotspot_array;
126 $panolist['scene-list'][] = $temp_array;
127 }
128 }
129 $panodata = $panolist;
130 return $panodata;
131 }
132
133
134 /**
135 * Remove empty scene and hotspot from panaromic data list
136 *
137 * @param array $panodata
138 *
139 * @return array
140 * @since 8.0.0
141 */
142 public function remove_empty_scene_and_hotspot($panodata)
143 {
144 $panolength = count($panodata["scene-list"]);
145 for ($i = 0; $i < $panolength; $i++) {
146 if (empty($panodata["scene-list"][$i]['scene-id'])) {
147 unset($panodata["scene-list"][$i]);
148 } else {
149 $panohotspotlength = count($panodata["scene-list"][$i]['hotspot-list']);
150 for ($j = 0; $j < $panohotspotlength; $j++) {
151 if (empty($panodata["scene-list"][$i]['hotspot-list'][$j]['hotspot-title'])) {
152 unset($panodata["scene-list"][$i]['hotspot-list'][$j]);
153 }
154 }
155 }
156 }
157 return $panodata;
158 }
159
160
161 /**
162 * Prepare tour rotation wrapper data
163 *
164 * @param array $pano_array
165 * @param string $rotation
166 *
167 * @return array
168 * @since 8.0.0
169 */
170 public function prepare_rotation_wrapper_data($pano_array, $rotation)
171 {
172 if ($rotation == 'off') {
173 unset($pano_array['autoRotate']);
174 unset($pano_array['autoRotateInactivityDelay']);
175 unset($pano_array['autoRotateStopDelay']);
176 }
177 if (empty($pano_array['autoRotate'])) {
178 unset($pano_array['autoRotate']);
179 unset($pano_array['autoRotateInactivityDelay']);
180 unset($pano_array['autoRotateStopDelay']);
181 }
182 if (empty($pano_array['autoRotateInactivityDelay'])) {
183 unset($pano_array['autoRotateInactivityDelay']);
184 }
185 if (empty($pano_array['autoRotateStopDelay'])) {
186 unset($pano_array['autoRotateStopDelay']);
187 }
188 return $pano_array;
189 }
190
191
192 public function prepare_video_settings_data()
193 {
194 $autoplay = '';
195 if (isset($_POST['autoplay'])) {
196 $autoplay = sanitize_text_field($_POST['autoplay']);
197 }
198
199 $loop = '';
200 if (isset($_POST['loop'])) {
201 $loop = sanitize_text_field($_POST['loop']);
202 }
203
204 return array(
205 'autoplay' => $autoplay,
206 'loop' => $loop
207 );
208 }
209
210
211 /**
212 * Prepare meta data when video url has youtube
213 *
214 * @param string $videourl
215 *
216 * @return string
217 * @since 8.0.0
218 */
219 public function prepare_youtube_video_meta_data($videourl, $videodata)
220 {
221 $explodeid = '';
222 $explodeid = explode("=", $videourl);
223
224 if ($videodata['autoplay'] == 'on') {
225 $autoplay = '&autoplay=1';
226 } else {
227 $autoplay = '';
228 }
229
230 if ($videodata['loop'] == 'on') {
231 $loop = '&loop=1';
232 } else {
233 $loop = '';
234 }
235
236 $foundid = '';
237 $foundid = $explodeid[1] . '?' . $autoplay . $loop;
238 $html = '';
239 $html .= '<iframe width="600" height="400" src="https://www.youtube.com/embed/' . $foundid . '" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>';
240 return $html;
241 }
242
243 /**
244 * Prepare meta data when video url has youtu.be
245 *
246 * @param string $videourl
247 *
248 * @return string
249 * @since 8.0.0
250 */
251 public function prepare_youtu_be_video_meta_data($videourl, $videodata)
252 {
253 $explodeid = '';
254 $explodeid = explode("/", $videourl);
255
256 if ($videodata['autoplay'] == 'on') {
257 $autoplay = '&autoplay=1';
258 } else {
259 $autoplay = '';
260 }
261
262 if ($videodata['loop'] == 'on') {
263 $loop = '&loop=1';
264 } else {
265 $loop = '';
266 }
267
268 $foundid = '';
269 $foundid = $explodeid[3] . '?' . $autoplay . $loop;
270 $html = '';
271 $html .= '<iframe width="600" height="400" src="https://www.youtube.com/embed/' . $foundid . '" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>';
272 return $html;
273 }
274
275 /**
276 * Prepare meta data when video url has vimeo
277 *
278 * @param string $videourl
279 *
280 * @return string
281 * @since 8.0.0
282 */
283 public function prepare_vimeo_video_meta_data($videourl, $videodata)
284 {
285 $vid_parts = explode( '/', rtrim( $videourl, '/' ) );
286 $vid_id = end( $vid_parts );
287
288 $do_autoplay = ( $videodata['autoplay'] === 'on' ) ? 'true' : 'false';
289 $do_loop = ( $videodata['loop'] === 'on' ) ? 'true' : 'false';
290
291 $autoplay_param = ( $videodata['autoplay'] === 'on' ) ? 'autoplay=1&muted=1' : '';
292 $loop_param = '';
293
294 $query_parts = array_filter( [ $autoplay_param, $loop_param ] );
295 $foundid = ! empty( $query_parts ) ? $vid_id . '?' . implode( '&', $query_parts ) : $vid_id;
296
297 $iframe_id = 'wpvr-vimeo-preview-' . wp_wp_rand( 10000, 99999 );
298
299 $html = '';
300 $html .= '<iframe id="' . esc_attr( $iframe_id ) . '" src="https://player.vimeo.com/video/' . esc_attr( $foundid ) . '" width="600" height="400" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>';
301 $html .= '<script>
302 (function() {
303 var IFRAME_ID = ' . wp_json_encode( $iframe_id ) . ';
304 var DO_AUTOPLAY = ' . $do_autoplay . ';
305 var DO_LOOP = ' . $do_loop . ';
306
307 function wpvrLoadVimeoSdk( callback ) {
308 if ( window.Vimeo && window.Vimeo.Player ) { callback(); return; }
309 window.wpvrVimeoSdkCallbacks = window.wpvrVimeoSdkCallbacks || [];
310 window.wpvrVimeoSdkCallbacks.push( callback );
311 if ( window.wpvrVimeoSdkLoading ) { return; }
312 window.wpvrVimeoSdkLoading = true;
313 var tag = document.createElement( "script" );
314 tag.src = "https://player.vimeo.com/api/player.js";
315 tag.onload = function() {
316 var cbs = window.wpvrVimeoSdkCallbacks || [];
317 while ( cbs.length ) { ( cbs.shift() )(); }
318 };
319 document.head.appendChild( tag );
320 }
321
322 function initVimeoPlayer() {
323 var iframeEl = document.getElementById( IFRAME_ID );
324 if ( !iframeEl ) { return; }
325 var player = new Vimeo.Player( iframeEl );
326
327 player.ready().then( function() {
328 if ( DO_LOOP ) {
329 player.setLoop( true ).catch( function() {} );
330 }
331 if ( DO_AUTOPLAY ) {
332 player.setVolume( 0 ).then( function() {
333 player.play().catch( function() {} );
334 } ).catch( function() {
335 player.play().catch( function() {} );
336 } );
337 }
338 } );
339 }
340
341 if ( document.readyState === "loading" ) {
342 document.addEventListener( "DOMContentLoaded", function() { wpvrLoadVimeoSdk( initVimeoPlayer ); } );
343 } else {
344 wpvrLoadVimeoSdk( initVimeoPlayer );
345 }
346 }());
347 </script>';
348 return $html;
349 }
350
351 /**
352 * Prepare meta data when video is selfhosted
353 *
354 * @param string $videourl
355 *
356 * @return string
357 * @since 8.0.0
358 */
359 public function prepare_selfhost_video_meta_data($videourl, $vidid, $videodata)
360 {
361 if ($videodata['autoplay'] == 'on') {
362 $autoplay = 'autoplay muted';
363 } else {
364 $autoplay = '';
365 }
366
367 if ($videodata['loop'] == 'on') {
368 $loop = 'loop';
369 } else {
370 $loop = '';
371 }
372
373 $html = '';
374 $html .= '<video id="' . $vidid . '" class="video-js vjs-default-skin vjs-big-play-centered" ' . $autoplay . ' ' . $loop . ' controls preload="auto" style="width:100%; height: 100%;" poster="" >';
375 $html .= '<source src="' . $videourl . '" type="video/mp4"/>';
376 $html .= '<p class="vjs-no-js">';
377 $html .= 'To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com html5-video-support/" target="_blank">supports HTML5 video</a>';
378 $html .= '</p>';
379 $html .= '</video>';
380 return $html;
381 }
382
383
384 /**
385 * Prepare scene data for Tour preview
386 *
387 * @param array $panodata
388 *
389 * @return array
390 * @since 8.0.0
391 */
392 public function prepare_scene_data_for_preview($panodata)
393 {
394 $scene_data = array();
395 foreach ($panodata["scene-list"] as $panoscenes) {
396 if (!empty($panoscenes['scene-id'])) {
397 $scene_ititle = sanitize_text_field(@$panoscenes["scene-ititle"]);
398 $scene_author = sanitize_text_field(@$panoscenes["scene-author"]);
399
400 $scene_vaov = isset($panoscenes["scene-vaov"]) ? (float)$panoscenes["scene-vaov"] : 180;
401 $scene_haov = isset($panoscenes["scene-vaov"]) ? (float)$panoscenes["scene-haov"] : 360;
402 $scene_vertical_offset = isset($panoscenes["scene-vertical-offset"]) ? (float)$panoscenes["scene-vertical-offset"] : 0;
403
404 $default_scene_pitch = isset($panoscenes["scene-pitch"]) ? (float)$panoscenes["scene-pitch"] : null;
405 $default_scene_yaw = isset($panoscenes["scene-yaw"]) ? (float)$panoscenes["scene-yaw"] : null;
406 $scene_max_pitch = isset($panoscenes["scene-maxpitch"]) ? (float)$panoscenes["scene-maxpitch"] : '';
407 $scene_min_pitch = isset($panoscenes["scene-minpitch"]) ? (float)$panoscenes["scene-minpitch"] : '';
408 $scene_max_yaw = isset($panoscenes["scene-maxyaw"]) ? (float)$panoscenes["scene-maxyaw"] : '';
409 $scene_min_yaw = isset($panoscenes["scene-minyaw"]) ? (float)$panoscenes["scene-minyaw"] : '';
410
411 $default_zoom = isset($panoscenes["scene-zoom"]) ? $panoscenes["scene-zoom"] : 100;
412 if (!empty($default_zoom)) {
413 $default_zoom = isset($panoscenes["scene-zoom"]) ? (int)$panoscenes["scene-zoom"] : (int)$default_zoom;
414 } else {
415 $default_zoom = 100;
416 }
417
418 $max_zoom = isset($panoscenes["scene-maxzoom"]) ? $panoscenes["scene-maxzoom"] : 120;
419 if (!empty($max_zoom)) {
420 $max_zoom = isset($panoscenes["scene-maxzoom"]) ? (int)$panoscenes["scene-maxzoom"] : (int)$max_zoom;
421 } else {
422 $max_zoom = 120;
423 }
424
425 $min_zoom = isset($panoscenes["scene-minzoom"]) ? $panoscenes["scene-minzoom"] : 50;
426 if (!empty($min_zoom)) {
427 $min_zoom = isset($panoscenes["scene-minzoom"]) ? (int)$panoscenes["scene-minzoom"] : (int)$min_zoom;
428 } else {
429 $min_zoom = 50;
430 }
431
432 $hotspot_datas = $panoscenes["hotspot-list"];
433
434 $hotspots = $this->prepare_hotspot_data_for_preview($hotspot_datas);
435
436 $scene_info = array();
437 if ($panoscenes["scene-type"] == 'cubemap') {
438 $pano_type = 'cubemap';
439 $pano_attachment = array(
440 $panoscenes["scene-attachment-url-face0"],
441 $panoscenes["scene-attachment-url-face1"],
442 $panoscenes["scene-attachment-url-face2"],
443 $panoscenes["scene-attachment-url-face3"],
444 $panoscenes["scene-attachment-url-face4"],
445 $panoscenes["scene-attachment-url-face5"]
446 );
447
448 $scene_info = array("type" => $panoscenes["scene-type"], "cubeMap" => $pano_attachment, "pitch" => $default_scene_pitch, "maxPitch" => $scene_max_pitch, "minPitch" => $scene_min_pitch, "maxYaw" => $scene_max_yaw, "minYaw" => $scene_min_yaw, "yaw" => $default_scene_yaw, "hfov" => $default_zoom, "maxHfov" => $max_zoom, "minHfov" => $min_zoom, "title" => $scene_ititle, "author" => $scene_author, "vaov" => $scene_vaov, "haov" => $scene_haov, "vOffset" => $scene_vertical_offset, "hotSpots" => $hotspots);
449 } else {
450 $scene_info = array("type" => $panoscenes["scene-type"], "panorama" => $panoscenes["scene-attachment-url"], "pitch" => $default_scene_pitch, "maxPitch" => $scene_max_pitch, "minPitch" => $scene_min_pitch, "maxYaw" => $scene_max_yaw, "minYaw" => $scene_min_yaw, "yaw" => $default_scene_yaw, "hfov" => $default_zoom, "maxHfov" => $max_zoom, "minHfov" => $min_zoom, "title" => $scene_ititle, "author" => $scene_author, "vaov" => $scene_vaov, "haov" => $scene_haov, "vOffset" => $scene_vertical_offset, "hotSpots" => $hotspots);
451 }
452
453 if (empty($panoscenes["scene-ititle"])) {
454 unset($scene_info['title']);
455 }
456 if (empty($panoscenes["scene-author"])) {
457 unset($scene_info['author']);
458 }
459
460 if (empty($scene_vaov)) {
461 unset($scene_info['vaov']);
462 }
463
464 if (empty($scene_haov)) {
465 unset($scene_info['haov']);
466 }
467
468 if (empty($scene_vertical_offset)) {
469 unset($scene_info['vOffset']);
470 }
471
472 if (isset($panoscenes["cvgscene"])) {
473 if ($panoscenes["cvgscene"] == "off") {
474 unset($scene_info['maxPitch']);
475 unset($scene_info['minPitch']);
476 }
477 }
478
479 if (empty($panoscenes["scene-maxpitch"])) {
480 unset($scene_info['maxPitch']);
481 }
482
483 if (empty($panoscenes["scene-minpitch"])) {
484 unset($scene_info['minPitch']);
485 }
486
487 if (isset($panoscenes["chgscene"])) {
488 if ($panoscenes["chgscene"] == "off") {
489 unset($scene_info['maxYaw']);
490 unset($scene_info['minYaw']);
491 }
492 }
493 if (empty($panoscenes["scene-maxyaw"])) {
494 unset($scene_info['maxYaw']);
495 }
496
497 if (empty($panoscenes["scene-minyaw"])) {
498 unset($scene_info['minYaw']);
499 }
500
501 $scene_array = array();
502 $scene_array = array(
503 $panoscenes["scene-id"] => $scene_info
504 );
505 $scene_data[$panoscenes["scene-id"]] = $scene_info;
506 }
507 }
508 return $scene_data;
509 }
510
511
512 /**
513 * Preapre hotspot data for Tour preview
514 *
515 * @param array $hotspot_datas
516 *
517 * @return array
518 * @since 8.0.0
519 */
520 private function prepare_hotspot_data_for_preview($hotspot_datas)
521 {
522 $hotspots = array();
523 foreach ($hotspot_datas as $hotspot_data) {
524
525 if (!empty($hotspot_data["hotspot-title"])) {
526
527 $hotspot_type = $hotspot_data["hotspot-type"] !== 'scene' ? 'info' : $hotspot_data["hotspot-type"];
528 $hotspot_content = '';
529
530 ob_start();
531 do_action('wpvr_hotspot_content_admin', $hotspot_data);
532 $hotspot_content = ob_get_clean();
533
534
535 if (!$hotspot_content) $hotspot_content = $hotspot_data["hotspot-content"];
536
537
538 $hotspot_info = array(
539 "text" => $hotspot_data["hotspot-title"],
540 "pitch" => $hotspot_data["hotspot-pitch"],
541 "yaw" => $hotspot_data["hotspot-yaw"],
542 "type" => $hotspot_type,
543 "URL" => $hotspot_data["hotspot-url"],
544 "clickHandlerArgs" => $hotspot_content,
545 "createTooltipArgs" => $hotspot_data["hotspot-hover"],
546 "sceneId" => $hotspot_data["hotspot-scene"],
547 'hotspot_type' => $hotspot_data['hotspot-type']
548 );
549
550 array_push($hotspots, $hotspot_info);
551 if (empty($hotspot_data["hotspot-scene"])) {
552 unset($hotspot_info['targetPitch']);
553 unset($hotspot_info['targetYaw']);
554 }
555 }
556 }
557 return $hotspots;
558 }
559
560
561 /**
562 * Prepare youtube video for preview
563 *
564 * @param string $videourl
565 *
566 * @return string
567 * @since 8.0.0
568 */
569 public function prepare_youtube_video_preview($videourl, $videodata)
570 {
571 $muted = ($videodata['autoplay'] == 'on') ? '&mute=1' : '';
572 $autoplay = ($videodata['autoplay'] == 'on') ? '&autoplay=1' : '';
573 $loop = ($videodata['loop'] == 'on') ? '&loop=1' : '';
574 $origin = '&origin=' . rawurlencode( home_url() );
575
576 $expdata = $this->extract_youtube_video_id( $videourl );
577 $playlist = '&playlist=' . $expdata;
578
579 $preview_id = 'wpvr-preview-' . wp_rand(10000, 99999);
580
581 $html = '';
582 $html .= '<div id="' . esc_attr( $preview_id . '-container' ) . '" style="position:relative; width:100%; max-width:523px; height:400px;">';
583
584 // Iframe
585 $html .= '<div id="' . esc_attr( $preview_id . '-frame' ) . '" style="width:100%; height:100%;">';
586 $html .= '<iframe id="' . esc_attr( $preview_id . '-iframe' ) . '" src="https://www.youtube.com/embed/' . esc_attr( $expdata ) . '?rel=0&modestbranding=1' . $loop . '&autohide=1' . $muted . '&showinfo=0&controls=0' . $autoplay . $playlist . '&enablejsapi=1&playsinline=1&html5=1' . $origin . '" width="100%" height="100%" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; xr-spatial-tracking"></iframe>';
587 $html .= '</div>';
588
589 // Drag surface
590 $html .= '<div id="' . esc_attr( $preview_id . '-drag-surface' ) . '" style="position:absolute; top:0; left:0; right:0; bottom:48px; display:none; pointer-events:none; cursor:grab; z-index:3; background:transparent;"></div>';
591
592 // Custom control bar
593 $html .= '<div id="' . esc_attr( $preview_id . '-controls' ) . '" style="display:none; position:absolute; bottom:0; left:0; right:0; height:48px; z-index:5; pointer-events:auto; background:linear-gradient(transparent, rgba(0,0,0,0.7)); align-items:center; padding:0 8px; gap:6px; box-sizing:border-box;">';
594
595 // Play/Pause
596 $html .= '<button type="button" id="' . esc_attr( $preview_id . '-play-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0;" title="Play">';
597 $html .= '<svg id="' . esc_attr( $preview_id . '-play-icon' ) . '" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white"><path d="M8 5v14l11-7z"/></svg>';
598 $html .= '</button>';
599
600 // Time current
601 $html .= '<span id="' . esc_attr( $preview_id . '-time-current' ) . '" style="color:#fff; font-size:12px; font-family:Arial,sans-serif; min-width:36px; text-align:center; user-select:none;">0:00</span>';
602
603 // Progress bar
604 $html .= '<div id="' . esc_attr( $preview_id . '-progress-wrap' ) . '" style="flex:1; height:4px; background:rgba(255,255,255,0.3); border-radius:2px; cursor:pointer; position:relative;">';
605 $html .= '<div id="' . esc_attr( $preview_id . '-progress-buffered' ) . '" style="position:absolute; top:0; left:0; height:100%; background:rgba(255,255,255,0.4); border-radius:2px; pointer-events:none;"></div>';
606 $html .= '<div id="' . esc_attr( $preview_id . '-progress-bar' ) . '" style="position:absolute; top:0; left:0; height:100%; background:#ff0000; border-radius:2px; pointer-events:none;"></div>';
607 $html .= '<div id="' . esc_attr( $preview_id . '-progress-thumb' ) . '" style="position:absolute; top:50%; left:0; width:12px; height:12px; background:#ff0000; border-radius:50%; transform:translate(-50%,-50%); pointer-events:none; display:none;"></div>';
608 $html .= '</div>';
609
610 // Time duration
611 $html .= '<span id="' . esc_attr( $preview_id . '-time-duration' ) . '" style="color:#fff; font-size:12px; font-family:Arial,sans-serif; min-width:36px; text-align:center; user-select:none;">0:00</span>';
612
613 // Mute button
614 $html .= '<button id="' . esc_attr( $preview_id . '-mute-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0; position:relative;" title="Mute">';
615 $html .= '<svg id="' . esc_attr( $preview_id . '-vol-icon' ) . '" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>';
616 $html .= '</button>';
617
618 // Volume slider
619 $html .= '<style>#' . esc_attr( $preview_id ) . '-vol-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:12px;height:12px;border-radius:50%;background:#fff;cursor:pointer;margin-top:-4.5px;}#' . esc_attr( $preview_id ) . '-vol-slider::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:#fff;border:none;cursor:pointer;}#' . esc_attr( $preview_id ) . '-vol-slider::-webkit-slider-runnable-track{height:3px;border-radius:1.5px;}#' . esc_attr( $preview_id ) . '-vol-slider::-moz-range-track{height:3px;border-radius:1.5px;background:rgba(255,255,255,0.3);}</style>';
620 $html .= '<div id="' . esc_attr( $preview_id . '-vol-slider-wrap' ) . '" style="display:none; align-items:center; height:36px; padding:0 4px;">';
621 $html .= '<input id="' . esc_attr( $preview_id . '-vol-slider' ) . '" type="range" min="0" max="100" value="100" style="width:52px; height:3px; cursor:pointer; -webkit-appearance:none; appearance:none; background:linear-gradient(to right,#fff 100%,rgba(255,255,255,0.3) 100%); border-radius:1.5px; outline:none;" />';
622 $html .= '</div>';
623
624 // Fullscreen button
625 $html .= '<button id="' . esc_attr( $preview_id . '-fs-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0;" title="Toggle fullscreen">';
626 $html .= '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>';
627 $html .= '</button>';
628
629 $html .= '</div>'; // Close controls
630 $html .= '</div>'; // Close container
631
632 // Script
633 $html .= '<script>
634 (function() {
635 var playerInitialised = false;
636 var ytPlayer = null;
637 var dragSurface = document.getElementById("' . esc_js( $preview_id ) . '-drag-surface");
638 var isDragging = false;
639 var dragStartX = 0;
640 var dragStartY = 0;
641 var dragStartView = null;
642
643 function clamp(value, min, max) {
644 return Math.min(Math.max(value, min), max);
645 }
646
647 function getSphericalView() {
648 if (!ytPlayer || typeof ytPlayer.getSphericalProperties !== "function") return null;
649 var view = ytPlayer.getSphericalProperties();
650 return view && Object.keys(view).length ? view : null;
651 }
652
653 function setDragSurfaceEnabled(enabled) {
654 if (!dragSurface) return;
655 dragSurface.style.display = enabled ? "block" : "none";
656 dragSurface.style.pointerEvents = enabled ? "auto" : "none";
657 }
658
659 function getEventPoint(event) {
660 if (event.touches && event.touches.length) {
661 return event.touches[0];
662 }
663
664 if (event.changedTouches && event.changedTouches.length) {
665 return event.changedTouches[0];
666 }
667
668 return event;
669 }
670
671 function beginDrag(event) {
672 var point = getEventPoint(event);
673 var sphericalView = getSphericalView();
674
675 if (!sphericalView || !point) return;
676
677 event.preventDefault();
678 dragStartX = point.clientX;
679 dragStartY = point.clientY;
680 dragStartView = sphericalView;
681 dragSurface.style.cursor = "grabbing";
682 }
683
684 function updateDrag(event) {
685 var deltaX;
686 var deltaY;
687 var nextView;
688 var point = getEventPoint(event);
689
690 if (!point) {
691 return;
692 }
693
694 if (!isDragging && dragStartView) {
695 deltaX = point.clientX - dragStartX;
696 deltaY = point.clientY - dragStartY;
697 if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) > 5) isDragging = true;
698 }
699
700 if (!isDragging || !dragStartView || !ytPlayer || typeof ytPlayer.setSphericalProperties !== "function") return;
701
702 event.preventDefault();
703 deltaX = point.clientX - dragStartX;
704 deltaY = point.clientY - dragStartY;
705 nextView = {
706 yaw: (dragStartView.yaw || 0) + (deltaX * 0.2),
707 pitch: clamp((dragStartView.pitch || 0) + (deltaY * 0.2), -85, 85),
708 roll: dragStartView.roll || 0,
709 fov: dragStartView.fov || 100
710 };
711 ytPlayer.setSphericalProperties(nextView);
712 }
713
714 function endDrag() {
715 if (!isDragging && dragStartView && ytPlayer) {
716 var playerState = ytPlayer.getPlayerState();
717 if (playerState === 1) ytPlayer.pauseVideo();
718 else ytPlayer.playVideo();
719 }
720 isDragging = false;
721 dragStartView = null;
722 if (dragSurface) dragSurface.style.cursor = "grab";
723 }
724
725 function update360DragAvailability() {
726 var sphericalView = getSphericalView();
727 setDragSurfaceEnabled(!!sphericalView);
728 }
729
730 function schedule360AvailabilityChecks() {
731 update360DragAvailability();
732 window.setTimeout(update360DragAvailability, 500);
733 window.setTimeout(update360DragAvailability, 1500);
734 window.setTimeout(update360DragAvailability, 3000);
735 }
736
737 if (dragSurface) {
738 dragSurface.addEventListener("mousedown", function(event) {
739 beginDrag(event);
740 });
741
742 dragSurface.addEventListener("touchstart", function(event) {
743 beginDrag(event);
744 }, { passive: false });
745
746 dragSurface.addEventListener("dragstart", function(event) { event.preventDefault(); });
747
748 window.addEventListener("mousemove", function(event) {
749 updateDrag(event);
750 });
751
752 window.addEventListener("touchmove", function(event) {
753 updateDrag(event);
754 }, { passive: false });
755
756 window.addEventListener("mouseup", function() {
757 endDrag();
758 });
759
760 window.addEventListener("touchend", function() {
761 endDrag();
762 });
763
764 window.addEventListener("touchcancel", function() {
765 endDrag();
766 });
767 }
768
769 function wpvrLoadYouTubeApi(callback) {
770 window.wpvrYoutubeApiCallbacks = window.wpvrYoutubeApiCallbacks || [];
771 if (window.YT && window.YT.Player) { callback(); return; }
772 window.wpvrYoutubeApiCallbacks.push(callback);
773 if (window.wpvrYoutubeApiLoading) return;
774 window.wpvrYoutubeApiLoading = true;
775 var previousReady = window.onYouTubeIframeAPIReady;
776 window.onYouTubeIframeAPIReady = function() {
777 if (typeof previousReady === "function") previousReady();
778 var callbacks = window.wpvrYoutubeApiCallbacks || [];
779 while (callbacks.length) {
780 var queuedCallback = callbacks.shift();
781 if (typeof queuedCallback === "function") queuedCallback();
782 }
783 };
784 var tag = document.createElement("script");
785 tag.src = "https://www.youtube.com/iframe_api";
786 var firstScriptTag = document.getElementsByTagName("script")[0];
787 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
788 }
789
790 function initializeYouTubePlayer() {
791 if (playerInitialised) return;
792 wpvrLoadYouTubeApi(function() {
793 if (playerInitialised || !(window.YT && window.YT.Player)) return;
794 ytPlayer = new YT.Player("' . esc_js( $preview_id . '-iframe' ) . '", {
795 events: {
796 onReady: function() {
797 schedule360AvailabilityChecks();
798 startProgressLoop();
799 },
800 onStateChange: function() {
801 schedule360AvailabilityChecks();
802 startProgressLoop();
803 }
804 }
805 });
806 playerInitialised = true;
807 });
808 }
809
810 // === Custom control bar logic ===
811 var outerContainer = document.getElementById("' . esc_js( $preview_id ) . '-container");
812 var controlBar = document.getElementById("' . esc_js( $preview_id ) . '-controls");
813 var playBtn = document.getElementById("' . esc_js( $preview_id ) . '-play-btn");
814 var playIcon = document.getElementById("' . esc_js( $preview_id ) . '-play-icon");
815 var timeCurrent = document.getElementById("' . esc_js( $preview_id ) . '-time-current");
816 var timeDuration = document.getElementById("' . esc_js( $preview_id ) . '-time-duration");
817 var progressWrap = document.getElementById("' . esc_js( $preview_id ) . '-progress-wrap");
818 var progressBar = document.getElementById("' . esc_js( $preview_id ) . '-progress-bar");
819 var progressBuffered = document.getElementById("' . esc_js( $preview_id ) . '-progress-buffered");
820 var progressThumb = document.getElementById("' . esc_js( $preview_id ) . '-progress-thumb");
821 var fsBtn = document.getElementById("' . esc_js( $preview_id ) . '-fs-btn");
822 var frameEl = document.getElementById("' . esc_js( $preview_id ) . '-frame");
823 var muteBtn = document.getElementById("' . esc_js( $preview_id ) . '-mute-btn");
824 var volSlider = document.getElementById("' . esc_js( $preview_id ) . '-vol-slider");
825 var volSliderWrap = document.getElementById("' . esc_js( $preview_id ) . '-vol-slider-wrap");
826 var volIcon = document.getElementById("' . esc_js( $preview_id ) . '-vol-icon");
827
828 var playPath = "M8 5v14l11-7z";
829 var pausePath = "M6 19h4V5H6v14zm8-14v14h4V5h-4z";
830 var volIconMuted = "M16.5 12A4.5 4.5 0 0014 8.14v2.07l2.45 2.45c.03-.21.05-.43.05-.66zm2.5 0c0 .93-.21 1.82-.58 2.61l1.47 1.47A8.94 8.94 0 0021 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a8.99 8.99 0 003.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z";
831 var volIconLow = "M7 9v6h4l5 5V4L7 9z";
832 var volIconMed = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86z";
833 var volIconHigh = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z";
834
835 function formatTime(sec) {
836 if (!sec || isNaN(sec)) return "0:00";
837 sec = Math.floor(sec);
838 var m = Math.floor(sec / 60);
839 var s = sec % 60;
840 return m + ":" + (s < 10 ? "0" : "") + s;
841 }
842
843 if (controlBar) controlBar.style.display = "flex";
844
845 // --- Play / Pause ---
846 if (playBtn) {
847 playBtn.addEventListener("click", function() {
848 if (!ytPlayer) return;
849 var state = ytPlayer.getPlayerState();
850 if (state === 1) ytPlayer.pauseVideo();
851 else ytPlayer.playVideo();
852 });
853 }
854
855 function updatePlayIcon() {
856 if (!ytPlayer || !playIcon) return;
857 try {
858 var state = ytPlayer.getPlayerState();
859 playIcon.querySelector("path").setAttribute("d", state === 1 ? pausePath : playPath);
860 } catch(e) {}
861 }
862
863 // --- Progress bar ---
864 var progressAnimId = null;
865 var isSeeking = false;
866 var progressLoopStarted = false;
867
868 function startProgressLoop() {
869 if (progressLoopStarted) return;
870 progressLoopStarted = true;
871 progressAnimId = requestAnimationFrame(updateProgress);
872 }
873
874 function updateProgress() {
875 if (!ytPlayer || isSeeking) { progressAnimId = requestAnimationFrame(updateProgress); return; }
876 try {
877 var current = typeof ytPlayer.getCurrentTime === "function" ? ytPlayer.getCurrentTime() : 0;
878 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
879 if (duration > 0) {
880 var pct = (current / duration) * 100;
881 progressBar.style.width = pct + "%";
882 progressThumb.style.left = pct + "%";
883 timeCurrent.textContent = formatTime(current);
884 timeDuration.textContent = formatTime(duration);
885 }
886 if (typeof ytPlayer.getVideoLoadedFraction === "function") {
887 progressBuffered.style.width = (ytPlayer.getVideoLoadedFraction() * 100) + "%";
888 }
889 updatePlayIcon();
890 } catch (e) {}
891 progressAnimId = requestAnimationFrame(updateProgress);
892 }
893
894 if (progressWrap) {
895 function seekFromEvent(e) {
896 var rect = progressWrap.getBoundingClientRect();
897 var x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
898 var pct = x / rect.width;
899 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
900 if (duration > 0 && ytPlayer) {
901 ytPlayer.seekTo(pct * duration, true);
902 progressBar.style.width = (pct * 100) + "%";
903 progressThumb.style.left = (pct * 100) + "%";
904 timeCurrent.textContent = formatTime(pct * duration);
905 }
906 }
907 progressWrap.addEventListener("mousedown", function(e) {
908 isSeeking = true;
909 seekFromEvent(e);
910 function onMove(e) { seekFromEvent(e); }
911 function onUp() {
912 isSeeking = false;
913 document.removeEventListener("mousemove", onMove);
914 document.removeEventListener("mouseup", onUp);
915 }
916 document.addEventListener("mousemove", onMove);
917 document.addEventListener("mouseup", onUp);
918 });
919 progressWrap.addEventListener("mouseenter", function() { progressThumb.style.display = "block"; });
920 progressWrap.addEventListener("mouseleave", function() { if (!isSeeking) progressThumb.style.display = "none"; });
921 }
922
923 // --- Volume ---
924 var volHideTimer = null;
925 var localVol = 100;
926 var localMuted = false;
927
928 function updateVolIconFromLocal() {
929 if (!volIcon) return;
930 var path;
931 if (localMuted || localVol === 0) path = volIconMuted;
932 else if (localVol < 40) path = volIconLow;
933 else if (localVol < 75) path = volIconMed;
934 else path = volIconHigh;
935 volIcon.querySelector("path").setAttribute("d", path);
936 }
937
938 function updateVolIcon() { updateVolIconFromLocal(); }
939
940 function showVolSlider() {
941 if (volHideTimer) { clearTimeout(volHideTimer); volHideTimer = null; }
942 if (volSliderWrap) volSliderWrap.style.display = "flex";
943 }
944 function hideVolSlider() {
945 volHideTimer = setTimeout(function() { if (volSliderWrap) volSliderWrap.style.display = "none"; }, 400);
946 }
947
948 if (muteBtn) {
949 muteBtn.addEventListener("click", function() {
950 if (!ytPlayer) return;
951 if (localMuted) {
952 ytPlayer.unMute(); localMuted = false;
953 if (localVol === 0) { localVol = 100; ytPlayer.setVolume(100); }
954 if (volSlider) volSlider.value = localVol;
955 } else {
956 ytPlayer.mute(); localMuted = true;
957 if (volSlider) volSlider.value = 0;
958 }
959 updateVolIconFromLocal();
960 updateVolSliderFill();
961 });
962 muteBtn.addEventListener("mouseenter", showVolSlider);
963 muteBtn.addEventListener("mouseleave", hideVolSlider);
964 }
965 if (volSliderWrap) {
966 volSliderWrap.addEventListener("mouseenter", showVolSlider);
967 volSliderWrap.addEventListener("mouseleave", hideVolSlider);
968 }
969 function updateVolSliderFill() {
970 if (!volSlider) return;
971 var v = parseInt(volSlider.value, 10);
972 volSlider.style.background = "linear-gradient(to right,#fff " + v + "%,rgba(255,255,255,0.3) " + v + "%)";
973 }
974 if (volSlider) {
975 volSlider.addEventListener("input", function() {
976 if (!ytPlayer) return;
977 var val = parseInt(volSlider.value, 10);
978 localVol = val;
979 ytPlayer.setVolume(val);
980 if (val === 0) { ytPlayer.mute(); localMuted = true; }
981 else if (localMuted) { ytPlayer.unMute(); localMuted = false; }
982 updateVolIconFromLocal();
983 updateVolSliderFill();
984 });
985 updateVolSliderFill();
986 }
987
988 // --- Fullscreen ---
989 if (outerContainer) {
990 var savedFrameWidth = frameEl ? frameEl.style.width : "";
991 var savedFrameHeight = frameEl ? frameEl.style.height : "";
992 var savedFrameMaxWidth = frameEl ? frameEl.style.maxWidth : "";
993 var savedContainerWidth = outerContainer.style.width;
994 var savedContainerHeight = outerContainer.style.height;
995 var savedContainerBackground = outerContainer.style.background;
996
997 if(fsBtn){
998 fsBtn.addEventListener("click", function() {
999 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
1000 if (fsEl) {
1001 (document.exitFullscreen || document.webkitExitFullscreen).call(document);
1002 } else {
1003 (outerContainer.requestFullscreen || outerContainer.webkitRequestFullscreen).call(outerContainer);
1004 }
1005 });
1006 }
1007
1008 function onFsChange() {
1009 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
1010 var isFs = (fsEl === outerContainer);
1011 frameEl.style.width = isFs ? "100%" : savedFrameWidth;
1012 frameEl.style.height = isFs ? "100%" : savedFrameHeight;
1013 frameEl.style.maxWidth = isFs ? "100%" : savedFrameMaxWidth;
1014 outerContainer.style.width = isFs ? "100%" : savedContainerWidth;
1015 outerContainer.style.height = isFs ? "100%" : savedContainerHeight;
1016 outerContainer.style.background = isFs ? "#000" : savedContainerBackground;
1017 fsBtn.querySelector("svg path").setAttribute("d", isFs
1018 ? "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"
1019 : "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
1020 );
1021 schedule360AvailabilityChecks();
1022 }
1023 document.addEventListener("fullscreenchange", onFsChange);
1024 document.addEventListener("webkitfullscreenchange", onFsChange);
1025 }
1026
1027 // Sync controls after player ready
1028 var origSchedule = schedule360AvailabilityChecks;
1029 schedule360AvailabilityChecks = function() {
1030 origSchedule();
1031 updateVolIcon();
1032 updatePlayIcon();
1033 };
1034
1035 // Initialize immediately for admin preview
1036 initializeYouTubePlayer();
1037 })();
1038 </script>';
1039
1040 return $html;
1041 }
1042
1043
1044 /**
1045 * Extract YouTube video ID from various URL formats.
1046 *
1047 * Handles youtube.com/watch?v=, youtu.be/, youtube.com/embed/, and youtube.com/v/ formats.
1048 * Strips extra query parameters and sanitizes the ID to only valid YouTube characters.
1049 *
1050 * @param string $url The YouTube video URL.
1051 *
1052 * @return string The sanitized YouTube video ID, or empty string on failure.
1053 * @since 8.0.0
1054 */
1055 private function extract_youtube_video_id( $url ) {
1056 $url = trim( $url );
1057 $parsed = wp_parse_url( $url );
1058
1059 // Handle youtu.be short URLs.
1060 if ( isset( $parsed['host'] ) && 'youtu.be' === $parsed['host'] ) {
1061 $video_id = isset( $parsed['path'] ) ? ltrim( $parsed['path'], '/' ) : '';
1062 return preg_replace( '/[^A-Za-z0-9_\-]/', '', $video_id );
1063 }
1064
1065 // Handle youtube.com/embed/VIDEO_ID or youtube.com/v/VIDEO_ID.
1066 if ( isset( $parsed['path'] ) && preg_match( '#/(embed|v)/([A-Za-z0-9_\-]+)#', $parsed['path'], $matches ) ) {
1067 return $matches[2];
1068 }
1069
1070 // Handle youtube.com/watch?v=VIDEO_ID (standard format).
1071 if ( isset( $parsed['query'] ) ) {
1072 parse_str( $parsed['query'], $query_params );
1073 if ( ! empty( $query_params['v'] ) ) {
1074 return preg_replace( '/[^A-Za-z0-9_\-]/', '', $query_params['v'] );
1075 }
1076 }
1077
1078 // Fallback: try naive extraction for edge cases.
1079 if ( preg_match( '/[?&]v=([A-Za-z0-9_\-]+)/', $url, $matches ) ) {
1080 return $matches[1];
1081 }
1082
1083 return '';
1084 }
1085
1086
1087 /**
1088 * Prepare shortcode data for youtube video url
1089 *
1090 * @param array $postdata
1091 * @param mixed $width
1092 * @param mixed $height
1093 *
1094 * @return string
1095 * @since 8.0.0
1096 */
1097 public function prepare_youtube_video_shortcode_data($postdata, $width, $height, $autoplay, $loop, $radius)
1098 {
1099 $muted = ($autoplay == 'on') ? '&mute=1' : '';
1100 $autoplay = ($autoplay == 'on') ? '&autoplay=1' : '';
1101 $loop = ($loop == 'on') ? '&loop=1' : '';
1102 $origin = '&origin=' . rawurlencode( home_url() );
1103
1104 $expdata = $this->extract_youtube_video_id( $postdata['vidurl'] );
1105
1106 $playlist = '&playlist=' . $expdata;
1107
1108 $html = '';
1109 $html .= '<div style="text-align:center; max-width:100%; width:' . $width . '; height:' . $height . '; border-radius: ' . $radius . '; margin: 0 auto;">';
1110
1111 // Add browser compatibility detection script
1112 $html .= '<script>
1113 function wpvr_check_360_support() {
1114 var support = {
1115 supported: false,
1116 fullySupported: false,
1117 webgl: false,
1118 orientation: false,
1119 gyroscope: false,
1120 touch: false,
1121 browser: "unknown",
1122 isMobile: false,
1123 isIPhone: false,
1124 isAndroid: false,
1125 details: []
1126 };
1127
1128 // Detect mobile devices
1129 support.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
1130 support.isIPhone = /iPhone/i.test(navigator.userAgent);
1131 support.isAndroid = /Android/i.test(navigator.userAgent);
1132 if (support.isMobile) {
1133 support.details.push("Mobile device detected");
1134 }
1135 if (support.isIPhone) {
1136 support.details.push("iPhone detected");
1137 }
1138 if (support.isAndroid) {
1139 support.details.push("Android device detected");
1140 }
1141
1142 // Browser detection
1143 var ua = navigator.userAgent;
1144 if (/^((?!chrome|android).)*safari/i.test(ua)) {
1145 support.browser = "safari";
1146 support.details.push("Safari browser detected");
1147 } else if (ua.indexOf("Chrome") > -1) {
1148 support.browser = "chrome";
1149 support.details.push("Chrome browser detected");
1150 } else if (ua.indexOf("Firefox") > -1) {
1151 support.browser = "firefox";
1152 support.details.push("Firefox browser detected");
1153 } else if (ua.indexOf("MSIE") > -1 || ua.indexOf("Trident") > -1) {
1154 support.browser = "ie";
1155 support.details.push("Internet Explorer detected");
1156 } else if (ua.indexOf("Edge") > -1 || ua.indexOf("Edg") > -1) {
1157 support.browser = "edge";
1158 support.details.push("Edge browser detected");
1159 } else if (ua.indexOf("Opera") > -1 || ua.indexOf("OPR") > -1) {
1160 support.browser = "opera";
1161 support.details.push("Opera browser detected");
1162 }
1163
1164 // Check WebGL support
1165 try {
1166 var canvas = document.createElement("canvas");
1167 support.webgl = !!(window.WebGLRenderingContext &&
1168 (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")));
1169
1170 if (support.webgl) {
1171 support.details.push("WebGL supported");
1172 } else {
1173 support.details.push("WebGL not supported");
1174 }
1175 } catch (e) {
1176 support.webgl = false;
1177 support.details.push("WebGL detection error: " + e.message);
1178 }
1179
1180 // Check device orientation support
1181 support.orientation = !!(window.DeviceOrientationEvent);
1182 if (support.orientation) {
1183 support.details.push("Device Orientation API supported");
1184 } else {
1185 support.details.push("Device Orientation API not supported");
1186 }
1187
1188 // Check for gyroscope
1189 if (window.Gyroscope) {
1190 support.gyroscope = true;
1191 support.details.push("Gyroscope supported");
1192 } else {
1193 support.details.push("Gyroscope not supported");
1194 }
1195
1196 // Check touch support for mobile interaction
1197 support.touch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
1198 if (support.touch) {
1199 support.details.push("Touch supported");
1200 } else {
1201 support.details.push("Touch not supported");
1202 }
1203
1204 // Overall support determination
1205 support.supported = support.webgl;
1206
1207 // Full support means WebGL + orientation on mobile or WebGL on desktop
1208 support.fullySupported = support.webgl;
1209
1210 // Browser-specific warnings
1211 if (support.browser === "ie") {
1212 support.browserWarning = "Internet Explorer has limited support for 360 videos. The experience may not be optimal.";
1213 support.fullySupported = false;
1214 } else if (!support.supported) {
1215 support.browserWarning = "Your browser does not fully support 360° videos. For the best experience, please use a modern browser with WebGL support.";
1216 }
1217
1218 return support;
1219 }
1220 </script>';
1221
1222 $random_id = 'video-container-' . wp_rand(10000, 99999);
1223 $html .= '<div id="' . $random_id . '-container" style="position:relative; width:100%; height:100%;">';
1224
1225 // First, always check browser compatibility before showing anything
1226 $html .= '<div id="' . $random_id . '-compatibility-check" style="position:absolute; top:0; left:0; width:100%; height:100%; display:none; pointer-events:none; flex-direction:column; justify-content:center; align-items:center; background-color:#f9f9f9; border-radius:' . $radius . ';">
1227 <div style="margin-bottom:20px; text-align:center;">
1228 <div class="wpvr-loading-spinner" style="border:5px solid #f3f3f3; border-top:5px solid #3498db; border-radius:50%; width:50px; height:50px; margin:0 auto 15px; animation:wpvr-spin 1s linear infinite;"></div>
1229 <p style="margin:0;">Checking browser compatibility...</p>
1230 </div>
1231 </div>';
1232
1233 // Add animation for spinner
1234 $html .= '<style>@keyframes wpvr-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>';
1235
1236 // Add the iframe (visible by default so YouTube 360° WebGL context gets correct dimensions)
1237 $html .= '<div id="' . esc_attr( $random_id . '-frame' ) . '" style="text-align:center; max-width:100%; width:' . esc_attr( $width ) . '; height:' . esc_attr( $height ) . '; border-radius:' . esc_attr( $radius ) . '; margin: 0 auto; display:block;">';
1238 $html .= '<iframe id="' . esc_attr( $random_id . '-iframe' ) . '" src="https://www.youtube.com/embed/' . esc_attr( $expdata ) . '?rel=0&modestbranding=1' . $loop . '&autohide=1' . $muted . '&showinfo=0&controls=0' . $autoplay . $playlist . '&enablejsapi=1&playsinline=1&html5=1' . $origin . '" width="100%" height="100%" style="border-radius:' . esc_attr( $radius ) . ';" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; xr-spatial-tracking"></iframe>';
1239 $html .= '</div>';
1240 $html .= '<div id="' . esc_attr( $random_id . '-drag-surface' ) . '" style="position:absolute; top:60px; left:0; right:0; bottom:48px; display:none; pointer-events:none; cursor:grab; z-index:3; background:transparent;"></div>';
1241
1242 // Custom control bar
1243 $html .= '<div id="' . esc_attr( $random_id . '-controls' ) . '" style="display:none; position:absolute; bottom:0; left:0; right:0; height:48px; z-index:5; pointer-events:auto; background:linear-gradient(transparent, rgba(0,0,0,0.7)); align-items:center; padding:0 8px; gap:6px; box-sizing:border-box;">';
1244
1245 // Play/Pause button
1246 $html .= '<button type="button" id="' . esc_attr( $random_id . '-play-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0;" title="Play">';
1247 $html .= '<svg id="' . esc_attr( $random_id . '-play-icon' ) . '" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white"><path d="M8 5v14l11-7z"/></svg>';
1248 $html .= '</button>';
1249
1250 // Time current
1251 $html .= '<span id="' . esc_attr( $random_id . '-time-current' ) . '" style="color:#fff; font-size:12px; font-family:Arial,sans-serif; min-width:36px; text-align:center; user-select:none;">0:00</span>';
1252
1253 // Progress bar
1254 $html .= '<div id="' . esc_attr( $random_id . '-progress-wrap' ) . '" style="flex:1; height:4px; background:rgba(255,255,255,0.3); border-radius:2px; cursor:pointer; position:relative;">';
1255 $html .= '<div id="' . esc_attr( $random_id . '-progress-buffered' ) . '" style="position:absolute; top:0; left:0; height:100%; background:rgba(255,255,255,0.4); border-radius:2px; pointer-events:none;"></div>';
1256 $html .= '<div id="' . esc_attr( $random_id . '-progress-bar' ) . '" style="position:absolute; top:0; left:0; height:100%; background:#ff0000; border-radius:2px; pointer-events:none;"></div>';
1257 $html .= '<div id="' . esc_attr( $random_id . '-progress-thumb' ) . '" style="position:absolute; top:50%; left:0; width:12px; height:12px; background:#ff0000; border-radius:50%; transform:translate(-50%,-50%); pointer-events:none; display:none;"></div>';
1258 $html .= '</div>';
1259
1260 // Time duration
1261 $html .= '<span id="' . esc_attr( $random_id . '-time-duration' ) . '" style="color:#fff; font-size:12px; font-family:Arial,sans-serif; min-width:36px; text-align:center; user-select:none;">0:00</span>';
1262
1263 // Mute button
1264 $html .= '<button id="' . esc_attr( $random_id . '-mute-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0; position:relative;" title="Mute">';
1265 $html .= '<svg id="' . esc_attr( $random_id . '-vol-icon' ) . '" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>';
1266 $html .= '</button>';
1267
1268 // Volume slider (popup on mute hover)
1269 $html .= '<style>#' . esc_attr( $random_id ) . '-vol-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#fff;cursor:pointer;margin-top:-5px;}#' . esc_attr( $random_id ) . '-vol-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#fff;border:none;cursor:pointer;}#' . esc_attr( $random_id ) . '-vol-slider::-webkit-slider-runnable-track{height:4px;border-radius:4px;}#' . esc_attr( $random_id ) . '-vol-slider::-moz-range-track{height:4px;border-radius:4px;background:rgba(255,255,255,0.3);}</style>';
1270 $html .= '<div id="' . esc_attr( $random_id . '-vol-slider-wrap' ) . '" style="display:none; align-items:center; height:40px; padding:0px 4px;">';
1271 $html .= '<input id="' . esc_attr( $random_id . '-vol-slider' ) . '" type="range" min="0" max="100" value="100" style="width:80px; height:4px; cursor:pointer; -webkit-appearance:none; appearance:none; background:linear-gradient(to right,#fff 100%,rgba(255,255,255,0.3) 100%); border-radius:4px; outline:none; padding:0;" />';
1272 $html .= '</div>';
1273
1274 // Fullscreen button
1275 $html .= '<button id="' . esc_attr( $random_id . '-fs-btn' ) . '" style="width:36px; height:36px; border:none; background:transparent; cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; flex-shrink:0;" title="Toggle fullscreen">';
1276 $html .= '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>';
1277 $html .= '</button>';
1278
1279 $html .= '</div>'; // Close controls
1280
1281 // Add browser compatibility warning message
1282 $html .= '<div id="' . $random_id . '-browser-warning" style="position:absolute; top:0; left:0; width:100%; height:100%; display:none; pointer-events:none; flex-direction:column; justify-content:center; align-items:center; background-color:rgba(0,0,0,0.7); color:white; text-align:center; border-radius:' . $radius . ';">';
1283 $html .= '<p id="' . $random_id . '-warning-text" style="font-size:16px; margin:0 20px 5px;"></p>';
1284 $html .= '<p style="font-size:14px; margin:5px 20px 15px;">For the best experience, use a browser with WebGL support.</p>';
1285 $html .= '<button id="' . $random_id . '-browser-continue" style="padding:10px 15px; background-color:#0085ba; color:#fff; border:none; border-radius:4px; cursor:pointer;">Continue Anyway</button>';
1286 $html .= '</div>';
1287
1288 $html .= '<div id="' . $random_id . '-iphone-fallback" style="position:absolute; top:0; left:0; width:100%; height:100%; display:none; pointer-events:none; flex-direction:column; justify-content:center; align-items:center; background-color:rgba(0,0,0,0.75); color:white; text-align:center; border-radius:' . $radius . ';">';
1289 $html .= '<p id="' . $random_id . '-mobile-fallback-title" style="font-size:16px; margin:0 20px 8px;">YouTube 360 is limited in mobile browser embeds.</p>';
1290 $html .= '<p id="' . $random_id . '-mobile-fallback-description" style="font-size:14px; margin:0 20px 15px;">For proper 360 playback, open this video in the YouTube app.</p>';
1291 $html .= '<a id="' . $random_id . '-iphone-open" href="https://www.youtube.com/watch?v=' . esc_attr( $expdata ) . '" target="_blank" rel="noopener noreferrer" style="color:rgba(255,255,255,0.85); text-decoration:underline; font-size:12px; margin-bottom:8px;">Open in YouTube</a>';
1292 $html .= '<a id="' . $random_id . '-iphone-continue" href="#" style="color:rgba(255,255,255,0.75); text-decoration:underline; font-size:12px;">Show Here Anyway</a>';
1293 $html .= '</div>';
1294
1295 // Main script for handling compatibility and permissions
1296 $html .= '<script>
1297 // Wait for DOM to be fully loaded
1298 document.addEventListener("DOMContentLoaded", function() {
1299 var playerInitialised = false;
1300 var ytPlayer = null;
1301 var dragSurface = document.getElementById("' . $random_id . '-drag-surface");
1302 var isDragging = false;
1303 var dragStartX = 0;
1304 var dragStartY = 0;
1305 var dragStartView = null;
1306
1307 function clamp(value, min, max) {
1308 return Math.min(Math.max(value, min), max);
1309 }
1310
1311 function getSphericalView() {
1312 if (!ytPlayer || typeof ytPlayer.getSphericalProperties !== "function") {
1313 return null;
1314 }
1315
1316 var view = ytPlayer.getSphericalProperties();
1317 return view && Object.keys(view).length ? view : null;
1318 }
1319
1320 function setDragSurfaceEnabled(enabled) {
1321 if (!dragSurface) {
1322 return;
1323 }
1324
1325 dragSurface.style.display = enabled ? "block" : "none";
1326 dragSurface.style.pointerEvents = enabled ? "auto" : "none";
1327 }
1328
1329 function getEventPoint(event) {
1330 if (event.touches && event.touches.length) {
1331 return event.touches[0];
1332 }
1333
1334 if (event.changedTouches && event.changedTouches.length) {
1335 return event.changedTouches[0];
1336 }
1337
1338 return event;
1339 }
1340
1341 function beginDrag(event) {
1342 var point = getEventPoint(event);
1343 var sphericalView = getSphericalView();
1344
1345 if (!sphericalView || !point) {
1346 return;
1347 }
1348
1349 event.preventDefault();
1350 dragStartX = point.clientX;
1351 dragStartY = point.clientY;
1352 dragStartView = sphericalView;
1353 dragSurface.style.cursor = "grabbing";
1354 }
1355
1356 function updateDrag(event) {
1357 var deltaX;
1358 var deltaY;
1359 var nextView;
1360 var point = getEventPoint(event);
1361
1362 if (!point) {
1363 return;
1364 }
1365
1366 if (!isDragging && dragStartView) {
1367 deltaX = point.clientX - dragStartX;
1368 deltaY = point.clientY - dragStartY;
1369 if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) > 5) {
1370 isDragging = true;
1371 }
1372 }
1373
1374 if (!isDragging || !dragStartView || !ytPlayer || typeof ytPlayer.setSphericalProperties !== "function") {
1375 return;
1376 }
1377
1378 event.preventDefault();
1379 deltaX = point.clientX - dragStartX;
1380 deltaY = point.clientY - dragStartY;
1381
1382 nextView = {
1383 yaw: (dragStartView.yaw || 0) + (deltaX * 0.2),
1384 pitch: clamp((dragStartView.pitch || 0) + (deltaY * 0.2), -85, 85),
1385 roll: dragStartView.roll || 0,
1386 fov: dragStartView.fov || 100
1387 };
1388
1389 ytPlayer.setSphericalProperties(nextView);
1390 }
1391
1392 function endDrag() {
1393 if (!isDragging && dragStartView && ytPlayer) {
1394 var playerState = ytPlayer.getPlayerState();
1395 if (playerState === 1) {
1396 ytPlayer.pauseVideo();
1397 } else {
1398 ytPlayer.playVideo();
1399 }
1400 }
1401 isDragging = false;
1402 dragStartView = null;
1403 if (dragSurface) {
1404 dragSurface.style.cursor = "grab";
1405 }
1406 }
1407
1408 function update360DragAvailability() {
1409 var sphericalView = getSphericalView();
1410 setDragSurfaceEnabled(!!sphericalView);
1411 }
1412
1413 function schedule360AvailabilityChecks() {
1414 update360DragAvailability();
1415 window.setTimeout(update360DragAvailability, 500);
1416 window.setTimeout(update360DragAvailability, 1500);
1417 window.setTimeout(update360DragAvailability, 3000);
1418 }
1419
1420 if (dragSurface) {
1421 dragSurface.addEventListener("mousedown", function(event) {
1422 beginDrag(event);
1423 });
1424
1425 dragSurface.addEventListener("touchstart", function(event) {
1426 beginDrag(event);
1427 }, { passive: false });
1428
1429 dragSurface.addEventListener("dragstart", function(event) {
1430 event.preventDefault();
1431 });
1432
1433 window.addEventListener("mousemove", function(event) {
1434 updateDrag(event);
1435 });
1436
1437 window.addEventListener("touchmove", function(event) {
1438 updateDrag(event);
1439 }, { passive: false });
1440
1441 window.addEventListener("mouseup", function() {
1442 endDrag();
1443 });
1444
1445 window.addEventListener("touchend", function() {
1446 endDrag();
1447 });
1448
1449 window.addEventListener("touchcancel", function() {
1450 endDrag();
1451 });
1452 }
1453
1454 function wpvrLoadYouTubeApi(callback) {
1455 window.wpvrYoutubeApiCallbacks = window.wpvrYoutubeApiCallbacks || [];
1456
1457 if (window.YT && window.YT.Player) {
1458 callback();
1459 return;
1460 }
1461
1462 window.wpvrYoutubeApiCallbacks.push(callback);
1463
1464 if (window.wpvrYoutubeApiLoading) {
1465 return;
1466 }
1467
1468 window.wpvrYoutubeApiLoading = true;
1469
1470 var previousReady = window.onYouTubeIframeAPIReady;
1471 window.onYouTubeIframeAPIReady = function() {
1472 if (typeof previousReady === "function") {
1473 previousReady();
1474 }
1475
1476 var callbacks = window.wpvrYoutubeApiCallbacks || [];
1477 while (callbacks.length) {
1478 var queuedCallback = callbacks.shift();
1479 if (typeof queuedCallback === "function") {
1480 queuedCallback();
1481 }
1482 }
1483 };
1484
1485 var tag = document.createElement("script");
1486 tag.src = "https://www.youtube.com/iframe_api";
1487 var firstScriptTag = document.getElementsByTagName("script")[0];
1488 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
1489 }
1490
1491 function initializeYouTubePlayer() {
1492 if (playerInitialised) {
1493 return;
1494 }
1495
1496 wpvrLoadYouTubeApi(function() {
1497 if (playerInitialised || !(window.YT && window.YT.Player)) {
1498 return;
1499 }
1500
1501 ytPlayer = new YT.Player("' . esc_js( $random_id . '-iframe' ) . '", {
1502 events: {
1503 onReady: function() {
1504 schedule360AvailabilityChecks();
1505 startProgressLoop();
1506 scheduleIPhoneFallbackCheck();
1507 },
1508 onStateChange: function() {
1509 schedule360AvailabilityChecks();
1510 startProgressLoop();
1511 scheduleIPhoneFallbackCheck();
1512 }
1513 }
1514 });
1515 playerInitialised = true;
1516 });
1517 }
1518
1519 // Check compatibility first
1520 var supportInfo = wpvr_check_360_support();
1521 var frameContainer = document.getElementById("' . $random_id . '-frame");
1522 let compatibilityCheck = document.getElementById("' . $random_id . '-compatibility-check");
1523 var iphoneFallback = document.getElementById("' . $random_id . '-iphone-fallback");
1524 var iphoneContinue = document.getElementById("' . $random_id . '-iphone-continue");
1525 var mobileFallbackTitle = document.getElementById("' . $random_id . '-mobile-fallback-title");
1526 var mobileFallbackDescription = document.getElementById("' . $random_id . '-mobile-fallback-description");
1527
1528 function updateMobileFallbackContent() {
1529 if (!mobileFallbackTitle || !mobileFallbackDescription) {
1530 return;
1531 }
1532
1533 if (supportInfo.isIPhone) {
1534 mobileFallbackTitle.textContent = "YouTube 360 is limited in iPhone Safari embeds.";
1535 mobileFallbackDescription.textContent = "For proper 360 playback, open this video in the YouTube app.";
1536 } else if (supportInfo.isAndroid) {
1537 mobileFallbackTitle.textContent = "YouTube 360 is limited in Android browser embeds.";
1538 mobileFallbackDescription.textContent = "For reliable 360 playback, open this video in the YouTube app.";
1539 } else {
1540 mobileFallbackTitle.textContent = "YouTube 360 is limited in mobile browser embeds.";
1541 mobileFallbackDescription.textContent = "For proper 360 playback, open this video in the YouTube app.";
1542 }
1543 }
1544
1545 function hideIPhoneFallback() {
1546 if (!iphoneFallback) {
1547 return;
1548 }
1549
1550 iphoneFallback.style.display = "none";
1551 iphoneFallback.style.pointerEvents = "none";
1552 }
1553
1554 function showIPhoneFallback() {
1555 if (!iphoneFallback) {
1556 return;
1557 }
1558
1559 frameContainer.style.display = "none";
1560 iphoneFallback.style.display = "flex";
1561 iphoneFallback.style.pointerEvents = "auto";
1562 }
1563
1564 function scheduleIPhoneFallbackCheck() {
1565 if (!supportInfo.isIPhone && !supportInfo.isAndroid) {
1566 return;
1567 }
1568
1569 updateMobileFallbackContent();
1570
1571 window.setTimeout(function() {
1572 if (!getSphericalView()) {
1573 showIPhoneFallback();
1574 }
1575 }, 1800);
1576 }
1577
1578 if (iphoneContinue) {
1579 iphoneContinue.addEventListener("click", function(event) {
1580 event.preventDefault();
1581 hideIPhoneFallback();
1582 showVideo();
1583 });
1584 }
1585
1586 if (supportInfo.isMobile && !supportInfo.fullySupported) {
1587 compatibilityCheck.style.display = "flex";
1588 compatibilityCheck.style.pointerEvents = "auto";
1589 }
1590 // Check browser compatibility and handle overlays accordingly.
1591 // The iframe is already visible by default so the YouTube 360° WebGL
1592 // renderer gets the correct dimensions during initialisation.
1593 setTimeout(function() {
1594 document.getElementById("' . $random_id . '-compatibility-check").style.display = "none";
1595 document.getElementById("' . $random_id . '-compatibility-check").style.pointerEvents = "none";
1596 // If browser doesn\'t support 360 videos well, hide the iframe and show warning
1597 if (!supportInfo.supported || supportInfo.browserWarning) {
1598 document.getElementById("' . $random_id . '-frame").style.display = "none";
1599 document.getElementById("' . $random_id . '-warning-text").textContent =
1600 supportInfo.browserWarning || "Your browser may not fully support 360° videos.";
1601 var warningEl = document.getElementById("' . $random_id . '-browser-warning");
1602 warningEl.style.display = "flex";
1603 warningEl.style.pointerEvents = "auto";
1604
1605 // Add continue anyway button handler
1606 document.getElementById("' . $random_id . '-browser-continue").addEventListener("click", function() {
1607 warningEl.style.display = "none";
1608 warningEl.style.pointerEvents = "none";
1609 showVideo();
1610 });
1611 }
1612 // Otherwise (supported desktop browser): iframe is already visible, nothing to do.
1613 else {
1614 initializeYouTubePlayer();
1615 }
1616 }, 500);
1617
1618 function showVideo() {
1619 hideIPhoneFallback();
1620 frameContainer.style.display = "block";
1621 initializeYouTubePlayer();
1622 }
1623
1624 // === Custom control bar logic ===
1625 var outerContainer = document.getElementById("' . $random_id . '-container");
1626 var controlBar = document.getElementById("' . $random_id . '-controls");
1627 var playBtn = document.getElementById("' . $random_id . '-play-btn");
1628 var playIcon = document.getElementById("' . $random_id . '-play-icon");
1629 var timeCurrent = document.getElementById("' . $random_id . '-time-current");
1630 var timeDuration = document.getElementById("' . $random_id . '-time-duration");
1631 var progressWrap = document.getElementById("' . $random_id . '-progress-wrap");
1632 var progressBar = document.getElementById("' . $random_id . '-progress-bar");
1633 var progressBuffered = document.getElementById("' . $random_id . '-progress-buffered");
1634 var progressThumb = document.getElementById("' . $random_id . '-progress-thumb");
1635 var fsBtn = document.getElementById("' . $random_id . '-fs-btn");
1636 var frameEl = document.getElementById("' . $random_id . '-frame");
1637 var muteBtn = document.getElementById("' . $random_id . '-mute-btn");
1638 var volSlider = document.getElementById("' . $random_id . '-vol-slider");
1639 var volSliderWrap = document.getElementById("' . $random_id . '-vol-slider-wrap");
1640 var volIcon = document.getElementById("' . $random_id . '-vol-icon");
1641
1642 var playPath = "M8 5v14l11-7z";
1643 var pausePath = "M6 19h4V5H6v14zm8-14v14h4V5h-4z";
1644 var volIconMuted = "M16.5 12A4.5 4.5 0 0014 8.14v2.07l2.45 2.45c.03-.21.05-.43.05-.66zm2.5 0c0 .93-.21 1.82-.58 2.61l1.47 1.47A8.94 8.94 0 0021 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a8.99 8.99 0 003.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z";
1645 var volIconLow = "M7 9v6h4l5 5V4L7 9z";
1646 var volIconMed = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86z";
1647 var volIconHigh = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.14v7.72c1.48-.73 2.5-2.25 2.5-3.86zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z";
1648
1649 function formatTime(sec) {
1650 if (!sec || isNaN(sec)) return "0:00";
1651 sec = Math.floor(sec);
1652 var m = Math.floor(sec / 60);
1653 var s = sec % 60;
1654 return m + ":" + (s < 10 ? "0" : "") + s;
1655 }
1656
1657 // Show control bar once player exists
1658 if (controlBar) controlBar.style.display = "flex";
1659
1660 // --- Play / Pause ---
1661 if (playBtn) {
1662 playBtn.addEventListener("click", function() {
1663 if (!ytPlayer) return;
1664 var state = ytPlayer.getPlayerState();
1665 if (state === 1) { ytPlayer.pauseVideo(); }
1666 else { ytPlayer.playVideo(); }
1667 });
1668 }
1669
1670 function updatePlayIcon() {
1671 if (!ytPlayer || !playIcon) return;
1672 var state = ytPlayer.getPlayerState();
1673 playIcon.querySelector("path").setAttribute("d", state === 1 ? pausePath : playPath);
1674 }
1675
1676 // --- Progress bar ---
1677 var progressAnimId = null;
1678 var isSeeking = false;
1679 var progressLoopStarted = false;
1680
1681 function startProgressLoop() {
1682 if (progressLoopStarted) return;
1683 progressLoopStarted = true;
1684 progressAnimId = requestAnimationFrame(updateProgress);
1685 }
1686
1687 function updateProgress() {
1688 if (!ytPlayer || isSeeking) { progressAnimId = requestAnimationFrame(updateProgress); return; }
1689 try {
1690 var current = typeof ytPlayer.getCurrentTime === "function" ? ytPlayer.getCurrentTime() : 0;
1691 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
1692 if (duration > 0) {
1693 var pct = (current / duration) * 100;
1694 progressBar.style.width = pct + "%";
1695 progressThumb.style.left = pct + "%";
1696 timeCurrent.textContent = formatTime(current);
1697 timeDuration.textContent = formatTime(duration);
1698 }
1699 if (typeof ytPlayer.getVideoLoadedFraction === "function") {
1700 progressBuffered.style.width = (ytPlayer.getVideoLoadedFraction() * 100) + "%";
1701 }
1702 updatePlayIcon();
1703 } catch (e) {
1704 // Player API not ready yet, retry on next frame
1705 }
1706 progressAnimId = requestAnimationFrame(updateProgress);
1707 }
1708
1709 // Seek on click / drag
1710 if (progressWrap) {
1711 function seekFromEvent(e) {
1712 var rect = progressWrap.getBoundingClientRect();
1713 var x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
1714 var pct = x / rect.width;
1715 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
1716 if (duration > 0 && ytPlayer) {
1717 ytPlayer.seekTo(pct * duration, true);
1718 progressBar.style.width = (pct * 100) + "%";
1719 progressThumb.style.left = (pct * 100) + "%";
1720 timeCurrent.textContent = formatTime(pct * duration);
1721 }
1722 }
1723
1724 progressWrap.addEventListener("mousedown", function(e) {
1725 isSeeking = true;
1726 seekFromEvent(e);
1727 function onMove(e) { seekFromEvent(e); }
1728 function onUp() {
1729 isSeeking = false;
1730 document.removeEventListener("mousemove", onMove);
1731 document.removeEventListener("mouseup", onUp);
1732 }
1733 document.addEventListener("mousemove", onMove);
1734 document.addEventListener("mouseup", onUp);
1735 });
1736
1737 progressWrap.addEventListener("mouseenter", function() {
1738 progressThumb.style.display = "block";
1739 });
1740 progressWrap.addEventListener("mouseleave", function() {
1741 if (!isSeeking) progressThumb.style.display = "none";
1742 });
1743 }
1744
1745 // --- Volume ---
1746 var volHideTimer = null;
1747 var localVol = 100;
1748 var localMuted = false;
1749
1750 function updateVolIconFromLocal() {
1751 if (!volIcon) return;
1752 var path;
1753 if (localMuted || localVol === 0) { path = volIconMuted; }
1754 else if (localVol < 40) { path = volIconLow; }
1755 else if (localVol < 75) { path = volIconMed; }
1756 else { path = volIconHigh; }
1757 volIcon.querySelector("path").setAttribute("d", path);
1758 }
1759
1760 // Called from schedule360AvailabilityChecks — only syncs icon, never touches slider
1761 function updateVolIcon() {
1762 updateVolIconFromLocal();
1763 }
1764
1765 function showVolSlider() {
1766 if (volHideTimer) { clearTimeout(volHideTimer); volHideTimer = null; }
1767 if (volSliderWrap) volSliderWrap.style.display = "flex";
1768 }
1769 function hideVolSlider() {
1770 volHideTimer = setTimeout(function() { if (volSliderWrap) volSliderWrap.style.display = "none"; }, 400);
1771 }
1772
1773 if (muteBtn) {
1774 muteBtn.addEventListener("click", function() {
1775 if (!ytPlayer) return;
1776 if (localMuted) {
1777 ytPlayer.unMute();
1778 localMuted = false;
1779 if (localVol === 0) { localVol = 100; ytPlayer.setVolume(100); }
1780 if (volSlider) volSlider.value = localVol;
1781 } else {
1782 ytPlayer.mute();
1783 localMuted = true;
1784 if (volSlider) volSlider.value = 0;
1785 }
1786 updateVolIconFromLocal();
1787 updateVolSliderFill();
1788 });
1789 muteBtn.addEventListener("mouseenter", showVolSlider);
1790 muteBtn.addEventListener("mouseleave", hideVolSlider);
1791 }
1792 if (volSliderWrap) {
1793 volSliderWrap.addEventListener("mouseenter", showVolSlider);
1794 volSliderWrap.addEventListener("mouseleave", hideVolSlider);
1795 }
1796 function updateVolSliderFill() {
1797 if (!volSlider) return;
1798 var v = parseInt(volSlider.value, 10);
1799 volSlider.style.background = "linear-gradient(to right,#fff " + v + "%,rgba(255,255,255,0.3) " + v + "%)";
1800 }
1801 if (volSlider) {
1802 volSlider.addEventListener("input", function() {
1803 if (!ytPlayer) return;
1804 var val = parseInt(volSlider.value, 10);
1805 localVol = val;
1806 ytPlayer.setVolume(val);
1807 if (val === 0) {
1808 ytPlayer.mute();
1809 localMuted = true;
1810 } else if (localMuted) {
1811 ytPlayer.unMute();
1812 localMuted = false;
1813 }
1814 updateVolIconFromLocal();
1815 updateVolSliderFill();
1816 });
1817 updateVolSliderFill();
1818 }
1819
1820 // --- Fullscreen ---
1821 if (outerContainer) {
1822 var savedFrameWidth = frameEl ? frameEl.style.width : "";
1823 var savedFrameHeight = frameEl ? frameEl.style.height : "";
1824 var savedFrameMaxWidth = frameEl ? frameEl.style.maxWidth : "";
1825 var savedFrameBorderRadius = frameEl ? frameEl.style.borderRadius : "";
1826 var savedContainerBackground = outerContainer.style.background;
1827 if(fsBtn){
1828 fsBtn.addEventListener("click", function() {
1829 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
1830 if (fsEl) {
1831 (document.exitFullscreen || document.webkitExitFullscreen).call(document);
1832 } else {
1833 (outerContainer.requestFullscreen || outerContainer.webkitRequestFullscreen).call(outerContainer);
1834 }
1835 });
1836 }
1837 function onFsChange() {
1838 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
1839 var isFs = (fsEl === outerContainer);
1840 frameEl.style.width = isFs ? "100%" : savedFrameWidth;
1841 frameEl.style.height = isFs ? "100%" : savedFrameHeight;
1842 frameEl.style.maxWidth = isFs ? "100%" : savedFrameMaxWidth;
1843 frameEl.style.borderRadius = isFs ? "0" : savedFrameBorderRadius;
1844 outerContainer.style.background = isFs ? "#000" : savedContainerBackground;
1845 fsBtn.querySelector("svg path").setAttribute("d", isFs
1846 ? "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"
1847 : "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
1848 );
1849 schedule360AvailabilityChecks();
1850 }
1851 document.addEventListener("fullscreenchange", onFsChange);
1852 document.addEventListener("webkitfullscreenchange", onFsChange);
1853 }
1854
1855 // Sync controls after player ready
1856 var origSchedule = schedule360AvailabilityChecks;
1857 schedule360AvailabilityChecks = function() {
1858 origSchedule();
1859 updateVolIcon();
1860 updatePlayIcon();
1861 };
1862 });
1863 </script>';
1864 $html .= '</div>'; // Close container
1865 $html .= '</div>'; // Close outer container
1866 return $html;
1867 }
1868 /**
1869 * Prepare shortcode data for Vimeo url
1870 *
1871 * @param array $postdata
1872 * @param mixed $width
1873 * @param mixed $height
1874 *
1875 * @return string
1876 * @since 8.0.0
1877 */
1878 public function prepare_vimeo_video_shortcode_data($postdata, $width, $height, $autoplay, $loop, $radius)
1879 {
1880 $autoplay_param = ($autoplay == 'on') ? 'autoplay=1&muted=1' : '';
1881 $loop_param = ($loop == 'on') ? 'loop=1' : '';
1882
1883 $vidurl_parts = explode('/', rtrim( $postdata['vidurl'], '/' ) );
1884 $vid_id = end($vidurl_parts);
1885
1886 $query_parts = array_filter( [ $autoplay_param, $loop_param ] );
1887 $foundid = !empty($query_parts) ? $vid_id . '?' . implode('&', $query_parts) : $vid_id;
1888
1889 $iframe_id = 'wpvr-vimeo-' . wp_wp_rand( 10000, 99999 );
1890 $do_autoplay = ( $autoplay == 'on' ) ? 'true' : 'false';
1891 $do_loop = ( $loop == 'on' ) ? 'true' : 'false';
1892
1893 $html = '';
1894 $html .= '<div style="text-align: center; max-width:100%; width:' . $width . '; height:' . $height . '; margin: 0 auto;">';
1895 $html .= '<iframe id="' . esc_attr( $iframe_id ) . '" src="https://player.vimeo.com/video/' . esc_attr( $foundid ) . '" width="' . trim($width, 'px') . '" height="' . trim($height, 'px') . '" style="border-radius: ' . $radius . ';" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>';
1896 $html .= '</div>';
1897 $html .= '<script>
1898 (function() {
1899 var IFRAME_ID = ' . wp_json_encode( $iframe_id ) . ';
1900 var DO_AUTOPLAY = ' . $do_autoplay . ';
1901 var DO_LOOP = ' . $do_loop . ';
1902
1903 function wpvrLoadVimeoSdk( callback ) {
1904 if ( window.Vimeo && window.Vimeo.Player ) { callback(); return; }
1905 window.wpvrVimeoSdkCallbacks = window.wpvrVimeoSdkCallbacks || [];
1906 window.wpvrVimeoSdkCallbacks.push( callback );
1907 if ( window.wpvrVimeoSdkLoading ) { return; }
1908 window.wpvrVimeoSdkLoading = true;
1909 var tag = document.createElement( "script" );
1910 tag.src = "https://player.vimeo.com/api/player.js";
1911 tag.onload = function() {
1912 var cbs = window.wpvrVimeoSdkCallbacks || [];
1913 while ( cbs.length ) { ( cbs.shift() )(); }
1914 };
1915 document.head.appendChild( tag );
1916 }
1917
1918 function initVimeoPlayer() {
1919 var iframeEl = document.getElementById( IFRAME_ID );
1920 if ( !iframeEl ) { return; }
1921 var player = new Vimeo.Player( iframeEl );
1922
1923 player.ready().then( function() {
1924 if ( DO_LOOP ) {
1925 player.setLoop( true ).catch( function() {} );
1926 }
1927 if ( DO_AUTOPLAY ) {
1928 player.setVolume( 0 ).then( function() {
1929 player.play().catch( function() {} );
1930 } ).catch( function() {
1931 player.play().catch( function() {} );
1932 } );
1933 }
1934 } );
1935 }
1936
1937 if ( document.readyState === "loading" ) {
1938 document.addEventListener( "DOMContentLoaded", function() { wpvrLoadVimeoSdk( initVimeoPlayer ); } );
1939 } else {
1940 wpvrLoadVimeoSdk( initVimeoPlayer );
1941 }
1942 }());
1943 </script>';
1944 return $html;
1945 }
1946
1947
1948 /**
1949 * Prepare shortcode data from regular postdata
1950 *
1951 * @param integer $id
1952 * @param array $postdata
1953 * @param string $width
1954 * @param string $height
1955 *
1956 * @return string
1957 * @since 8.0.0
1958 */
1959 public function prepare_regular_video_shortcode_data($id, $postdata, $width, $height, $radius)
1960 {
1961 $html = '';
1962 $html .= '<div id="pano' . $id . '" class="pano-wrap" style="max-width:100%; width: ' . $width . '; height: ' . $height . '; border-radius: ' . $radius . '; margin: 0 auto;">';
1963 $html .= '<div style="width:100%; height:100%; ">' . $postdata['panoviddata'] . '</div>';
1964
1965 $html .= '
1966 <style>
1967 .video-js {
1968 border-radius:' . $radius . ';
1969 }
1970 .video-js canvas{
1971 border-radius:' . $radius . ';
1972 }
1973 #pano' . $id . ' .vjs-poster {
1974 border-radius: ' . $radius . ';
1975 }
1976 </style>
1977
1978 ';
1979
1980 $html .= '</div>';
1981
1982 //video js vr setup //
1983 $html .= '<script>';
1984 $html .= '
1985 (function (window, videojs) {
1986 var player = window.player = videojs("' . $postdata['vidid'] . '");
1987 player.mediainfo = player.mediainfo || {};
1988 player.mediainfo.projection = "equirectangular";
1989
1990 // AUTO is the default and looks at mediainfo
1991 var vr = window.vr = player.vr({ projection: "AUTO", debug: true, forceCardboard: false, antialias: false });
1992 }(window, window.videojs));
1993
1994 ';
1995 $html .= '</script>';
1996 //video js vr end //
1997 return $html;
1998 }
1999
2000
2001 /**
2002 * Prepare scene data for shortcode
2003 *
2004 * @param array $panodata
2005 *
2006 * @return array
2007 * @since 8.0.0
2008 */
2009 public function prepare_shortcode_scene_data($panodata)
2010 {
2011 $scene_data = array();
2012
2013 if (!empty($panodata["scene-list"])) {
2014 $scene_data = $this->prepare_shortcode_scene_list($panodata);
2015 }
2016
2017 return $scene_data;
2018 }
2019
2020
2021 /**
2022 * Prepare scene lists data for shortcode
2023 *
2024 * @param array $panodata
2025 *
2026 * @return array
2027 * @since 8.0.0
2028 */
2029 private function prepare_shortcode_scene_list($panodata)
2030 {
2031 $scene_data = array();
2032 foreach ($panodata["scene-list"] as $panoscenes) {
2033 $hotspot_datas = array();
2034 if (isset($panoscenes["hotspot-list"])) {
2035 $hotspot_datas = $panoscenes["hotspot-list"];
2036 }
2037 $hotspots = $this->prepare_shortcode_hotspots_list($hotspot_datas);
2038 $device_scene = $this->get_shortcode_device_scene($panoscenes['scene-attachment-url']);
2039 $scene_info = $this->get_shortcode_scene_info($panoscenes, $hotspots, $device_scene);
2040 $scene_data[$panoscenes["scene-id"]] = $scene_info;
2041 }
2042 return $scene_data;
2043 }
2044
2045
2046 /**
2047 * Prepare hotspots list for shortcode
2048 *
2049 * @param array $hotspot_datas
2050 *
2051 * @return array
2052 * @since 8.0.0
2053 */
2054 private function prepare_shortcode_hotspots_list($hotspot_datas)
2055 {
2056 $hotspots = array();
2057 foreach ($hotspot_datas as $hotspot_data) {
2058 $hotspot_type = $hotspot_data["hotspot-type"] !== 'scene' ? 'info' : $hotspot_data["hotspot-type"];
2059 $hotspot_content = $hotspot_data["hotspot-content"];
2060 $hotspot_info = $this->get_shortcode_hotspot_info($hotspot_data, $hotspot_type, $hotspot_content);
2061 array_push($hotspots, $hotspot_info);
2062 }
2063 return $hotspots;
2064 }
2065
2066
2067 /**
2068 * Return hotspot info array to hotspot list
2069 *
2070 * @param array $hotspot_data
2071 * @param string $hotspot_type
2072 * @param string $hotspot_content
2073 *
2074 * @return array
2075 * @since 8.0.0
2076 */
2077 private function get_shortcode_hotspot_info($hotspot_data, $hotspot_type, $hotspot_content)
2078 {
2079 $hotspot_info = array(
2080 "text" => $hotspot_data["hotspot-title"],
2081 "pitch" => $hotspot_data["hotspot-pitch"],
2082 "yaw" => $hotspot_data["hotspot-yaw"],
2083 "type" => $hotspot_type,
2084 "cssClass" => $hotspot_data["hotspot-customclass"],
2085 "URL" => $hotspot_data["hotspot-url"],
2086 "wpvr_url_open" => isset($hotspot_data["wpvr_url_open"][0]) ? $hotspot_data["wpvr_url_open"][0] : 'off',
2087 "clickHandlerArgs" => $hotspot_content,
2088 "createTooltipArgs" => $hotspot_data["hotspot-hover"],
2089 "sceneId" => $hotspot_data["hotspot-scene"],
2090 'hotspot_type' => $hotspot_data['hotspot-type']
2091 );
2092
2093 $hotspot_info['URL'] = ($hotspot_data['hotspot-type'] === 'fluent_form' || $hotspot_data['hotspot-type'] === 'wc_product') ? '' : $hotspot_info['URL'];
2094
2095 if ($hotspot_data["hotspot-customclass"] == 'none' || $hotspot_data["hotspot-customclass"] == '') {
2096 unset($hotspot_info["cssClass"]);
2097 }
2098 if (empty($hotspot_data["hotspot-scene"])) {
2099 unset($hotspot_info['targetPitch']);
2100 unset($hotspot_info['targetYaw']);
2101 }
2102 return $hotspot_info;
2103 }
2104
2105
2106 /**
2107 * Return scene info array for scene list
2108 *
2109 * @param array $panoscenes
2110 * @param array $hotspots
2111 * @param string $device_scene
2112 *
2113 * @return array
2114 * @since 8.0.0
2115 */
2116 private function get_shortcode_scene_info($panoscenes, $hotspots, $device_scene)
2117 {
2118 $scene_info = array();
2119 if ($panoscenes["scene-type"] == 'cubemap') {
2120 $pano_attachment = array(
2121 $panoscenes["scene-attachment-url-face0"],
2122 $panoscenes["scene-attachment-url-face1"],
2123 $panoscenes["scene-attachment-url-face2"],
2124 $panoscenes["scene-attachment-url-face3"],
2125 $panoscenes["scene-attachment-url-face4"],
2126 $panoscenes["scene-attachment-url-face5"]
2127 );
2128 $scene_info = array("type" => $panoscenes["scene-type"], "cubeMap" => $pano_attachment, "hotSpots" => $hotspots);
2129 } else {
2130 $scene_info = array("type" => $panoscenes["scene-type"], "panorama" => $device_scene, "hotSpots" => $hotspots);
2131 }
2132 return $scene_info;
2133 }
2134
2135
2136 /**
2137 * Return device scene for shortcode
2138 *
2139 * @param string $device_scene
2140 *
2141 * @return string
2142 * @since 8.0.0
2143 */
2144 private function get_shortcode_device_scene($device_scene)
2145 {
2146 $mobile_media_resize = get_option('mobile_media_resize');
2147 $file_accessible = ini_get('allow_url_fopen');
2148
2149 if ($mobile_media_resize == "true" && $device_scene) {
2150 if ($file_accessible == "1") {
2151 $image_info = getimagesize($device_scene);
2152 if ($image_info[0] > 4096) {
2153 $src_to_id_for_mobile = '';
2154 $src_to_id_for_desktop = '';
2155 if (wpvr_isMobileDevice()) {
2156 $src_to_id_for_mobile = attachment_url_to_postid($device_scene);
2157 if ($src_to_id_for_mobile) {
2158 $mobile_scene = wp_get_attachment_image_src($src_to_id_for_mobile, 'wpvr_mobile');
2159 if ($mobile_scene[3]) {
2160 $device_scene = $mobile_scene[0];
2161 }
2162 }
2163 } else {
2164 $src_to_id_for_desktop = attachment_url_to_postid($device_scene);
2165 if ($src_to_id_for_desktop) {
2166 $desktop_scene = wp_get_attachment_image_src($src_to_id_for_mobile, 'full');
2167 if ($desktop_scene[0]) {
2168 $device_scene = $desktop_scene[0];
2169 }
2170 }
2171 }
2172 }
2173 }
2174 }
2175 return $device_scene;
2176 }
2177
2178
2179 /**
2180 * Prepare json response for shortcode
2181 *
2182 * @param string $autorotation
2183 * @param array $pano_response
2184 * @param string $panoid
2185 * @param integer $autorotationinactivedelay
2186 * @param integer $autorotationstopdelay
2187 *
2188 * @return array
2189 * @since 8.0.0
2190 */
2191 public function prepare_shortcode_response($autorotation, $pano_response, $panoid, $autorotationinactivedelay, $autorotationstopdelay)
2192 {
2193 $pano_id_array = array("panoid" => $panoid);
2194 if (empty($autorotation)) {
2195 unset($pano_response['autoRotate']);
2196 unset($pano_response['autoRotateInactivityDelay']);
2197 unset($pano_response['autoRotateStopDelay']);
2198 }
2199 if (empty($autorotationinactivedelay)) {
2200 unset($pano_response['autoRotateInactivityDelay']);
2201 }
2202 if (empty($autorotationstopdelay)) {
2203 unset($pano_response['autoRotateStopDelay']);
2204 }
2205 $response = array();
2206 $response = array($pano_id_array, $pano_response);
2207 if (!empty($response)) {
2208 $response = json_encode($response);
2209 }
2210 return $response;
2211 }
2212
2213
2214 /**
2215 * Prepare HTML and script contents for scene shortcode
2216 *
2217 * @param mixed $width
2218 * @param mixed $panoid
2219 * @param mixed $hotspoticoncolor
2220 * @param mixed $foreground_color
2221 * @param mixed $hotspotblink
2222 * @param mixed $pulse_color
2223 * @param mixed $radius
2224 * @param mixed $id
2225 * @param mixed $height
2226 * @param mixed $scene_data
2227 * @param mixed $response
2228 * @param mixed $autoload
2229 * @param mixed $default_scene
2230 * @param mixed $postdata
2231 *
2232 * @return string
2233 * @since 8.0.0
2234 */
2235 public function prepare_scene_shortcode_html_content($width, $panoid, $hotspoticoncolor, $foreground_color, $hotspotblink, $pulse_color, $radius, $id, $height, $scene_data, $response, $autoload, $default_scene, $postdata, $mobile_height, $gyro)
2236 {
2237 $html = '';
2238 $html .= '<style>';
2239
2240 if ($width == 'embed') {
2241 $html .= 'body{overflow: hidden;}';
2242 }
2243 // Return ID and Classes for CSS styling //
2244 $html = $this->get_shortcode_content_classes($html, $panoid, $hotspoticoncolor, $foreground_color);
2245 // Return webkit keyframes CSS styling //
2246 if ($hotspotblink == 'on') {
2247 $html = $this->get_shortcode_webkit_keyframes($html, $panoid, $pulse_color);
2248 }
2249 $status = get_option('wpvr_edd_license_status');
2250 if ($status !== false && $status == 'valid') {
2251 if (!$gyro) {
2252 $html .= '#' . $panoid . ' div.pnlm-orientation-button {
2253 display: none;
2254 }';
2255 }
2256 } else {
2257 $html .= '#' . $panoid . ' div.pnlm-orientation-button {
2258 display: none;
2259 }';
2260 }
2261 $html .= '</style>';
2262
2263 // Render pano wrapper contents //
2264 $html = $this->get_shortcode_pano_wrap_content($width, $radius, $html, $id, $height, $mobile_height);
2265 // Render hotspots contents //
2266 $html = $this->render_shortcode_hotspot_content($html, $id, $scene_data);
2267 // Render custom iframe content //
2268 $html = $this->render_shortcode_custom_iframe_wrapper($html, $id);
2269
2270 $html .= '</div>';
2271
2272 //script started
2273 $html = $this->get_shortcode_script_content($html, $response, $id, $autoload, $default_scene, $postdata);
2274 //script end
2275 return $html;
2276 }
2277
2278
2279 /**
2280 * Return ID and Classes for CSS styling
2281 *
2282 * @param mixed $html
2283 * @param mixed $panoid
2284 * @param mixed $hotspoticoncolor
2285 * @param mixed $foreground_color
2286 *
2287 * @return string
2288 * @since 8.0.0
2289 */
2290 private function get_shortcode_content_classes($html, $panoid, $hotspoticoncolor, $foreground_color)
2291 {
2292 $html .= '#' . $panoid . ' div.pnlm-hotspot-base.fas,
2293 #' . $panoid . ' div.pnlm-hotspot-base.fab,
2294 #' . $panoid . ' div.pnlm-hotspot-base.fa,
2295 #' . $panoid . ' div.pnlm-hotspot-base.fa-solid,
2296 #' . $panoid . ' div.pnlm-hotspot-base.far {
2297 display: block !important;
2298 background-color: ' . $hotspoticoncolor . ';
2299 color: ' . $foreground_color . ';
2300 border-radius: 100%;
2301 width: 30px;
2302 height: 30px;
2303 animation: icon-pulse' . $panoid . ' 1.5s infinite cubic-bezier(.25, 0, 0, 1);
2304 }';
2305 return $html;
2306 }
2307
2308
2309 /**
2310 * Resturn webkit keyframes CSS styling
2311 *
2312 * @param mixed $html
2313 * @param mixed $panoid
2314 * @param mixed $pulse_color
2315 *
2316 * @return string
2317 * @since 8.0.0
2318 */
2319 private function get_shortcode_webkit_keyframes($html, $panoid, $pulse_color)
2320 {
2321 $html .= '@-webkit-keyframes icon-pulse' . $panoid . ' {
2322 0% {
2323 box-shadow: 0 0 0 0px rgba(' . $pulse_color[0] . ', 1);
2324 }
2325 100% {
2326 box-shadow: 0 0 0 10px rgba(' . $pulse_color[0] . ', 0);
2327 }
2328 }
2329 @keyframes icon-pulse' . $panoid . ' {
2330 0% {
2331 box-shadow: 0 0 0 0px rgba(' . $pulse_color[0] . ', 1);
2332 }
2333 100% {
2334 box-shadow: 0 0 0 10px rgba(' . $pulse_color[0] . ', 0);
2335 }
2336 }';
2337 return $html;
2338 }
2339
2340
2341 /**
2342 * Render pano wrapper content depending on Tour Width, Height and radius
2343 *
2344 * @param mixed $width
2345 * @param mixed $radius
2346 * @param mixed $html
2347 * @param mixed $id
2348 * @param mixed $height
2349 *
2350 * @return string
2351 * @since 8.0.0
2352 */
2353 private function get_shortcode_pano_wrap_content($width, $radius, $html, $id, $height, $mobile_height)
2354 {
2355 if ($width == 'fullwidth') {
2356
2357 if (wpvr_isMobileDevice()) {
2358 if ($radius) {
2359 $html .= '<div id="pano' . $id . '" class="pano-wrap" style="text-align:center; border-radius:' . $radius . '; direction:ltr;">';
2360 } else {
2361 $html .= '<div id="pano' . $id . '" class="pano-wrap" style="text-align:center;">';
2362 }
2363 } else {
2364 if ($radius) {
2365 $html .= '<div id="pano' . $id . '" class="pano-wrap vrfullwidth" style=" text-align:center; height: ' . $height . '; border-radius:' . $radius . '; direction:ltr;" >';
2366 } else {
2367 $html .= '<div id="pano' . $id . '" class="pano-wrap vrfullwidth" style=" text-align:center; height: ' . $height . '; direction:ltr;" >';
2368 }
2369 }
2370 } elseif ($width == 'embed') {
2371 $html .= '<div id="pano' . $id . '" class="pano-wrap vrembed" style=" text-align:center; direction:ltr;" >';
2372 } else {
2373 if (wpvr_isMobileDevice()) {
2374 if ($radius) {
2375 $html .= '<div id="pano' . $id . '" class="pano-wrap" style=" text-align:center; max-width:100%; width: ' . $width . '; height: ' . $mobile_height . '!important; margin: 0 auto; border-radius:' . $radius . '; direction:ltr;">';
2376 } else {
2377 $html .= '<div id="pano' . $id . '" class="pano-wrap" style=" text-align:center; max-width:100%; width: ' . $width . '; height: ' . $mobile_height . '!important; margin: 0 auto; direction:ltr;">';
2378 }
2379 } else {
2380 if ($radius) {
2381 $html .= '<div id="pano' . $id . '" class="pano-wrap" style=" text-align:center; max-width:100%; width: ' . $width . '; height: ' . $height . '; margin: 0 auto; border-radius:' . $radius . '; direction:ltr;">';
2382 } else {
2383 $html .= '<div id="pano' . $id . '" class="pano-wrap" style=" text-align:center; max-width:100%; width: ' . $width . '; height: ' . $height . '; margin: 0 auto; direction:ltr;">';
2384 }
2385 }
2386 }
2387 return $html;
2388 }
2389
2390
2391 /**
2392 * Render hotspot contents inside pano wrapper
2393 *
2394 * @param string $html
2395 * @param mixed $id
2396 * @param array $scene_data
2397 *
2398 * @return string
2399 * @since 8.0.0
2400 */
2401 private function render_shortcode_hotspot_content($html, $id, $scene_data)
2402 {
2403 $html .= '<div class="wpvr-hotspot-tweak-contents-wrapper" style="display: none">';
2404 $html .= '<i class="fa fa-times cross" data-id="' . $id . '"></i>';
2405 $html .= '<div class="wpvr-hotspot-tweak-contents-flex">';
2406 $html .= '<div class="wpvr-hotspot-tweak-contents">';
2407 ob_start();
2408 do_action('wpvr_hotspot_tweak_contents', $scene_data);
2409 $hotspot_content = ob_get_clean();
2410 $html .= $hotspot_content;
2411 $html .= '</div>';
2412 $html .= '</div>';
2413 $html .= '</div>';
2414 return $html;
2415 }
2416
2417
2418 /**
2419 * Render custom iframe wrapper inside pano wrapper
2420 *
2421 * @param string $html
2422 * @param mixed $id
2423 *
2424 * @return string
2425 * @since 8.0.0
2426 */
2427 private function render_shortcode_custom_iframe_wrapper($html, $id)
2428 {
2429 $html .= '<div class="custom-ifram-wrapper" style="display: none;">';
2430 $html .= '<i class="fa fa-times cross" data-id="' . $id . '"></i>';
2431
2432 $html .= '<div class="custom-ifram-flex">';
2433 $html .= '<div class="custom-ifram">';
2434 $html .= '</div>';
2435 $html .= '</div>';
2436 $html .= '</div>';
2437 return $html;
2438 }
2439
2440
2441 /**
2442 * Return scripts contents for shortcode
2443 *
2444 * @param string $html
2445 * @param mixed $response
2446 * @param mixed $id
2447 * @param mixed $autoload
2448 * @param mixed $default_scene
2449 * @param mixed $postdata
2450 *
2451 * @return string
2452 * @since 8.0.0
2453 */
2454 private function get_shortcode_script_content($html, $response, $id, $autoload, $default_scene, $postdata)
2455 {
2456 $html .= '<script>';
2457 $html .= 'jQuery(document).ready(function() {';
2458 $html .= 'var response = ' . $response . ';';
2459 $html .= 'var scenes = response[1];';
2460 // Hotspot scripts content //
2461 $html = $this->get_shortcode_hotspot_script_content($html);
2462
2463 $html .= 'var panoshow' . $id . ' = pannellum.viewer(response[0]["panoid"], scenes);';
2464
2465 // Panaromic show scripts content //
2466 $html = $this->render_shortcode_panoshow_script_content($html, $id);
2467 // Auto rotation scripts content //
2468 $html = $this->render_shortcode_autoRotate_content($html, $id);
2469
2470 $html .= 'var touchtime = 0;';
2471
2472 // Return autoload scripts content //
2473 if (!$autoload) {
2474 $html = $this->render_shortcode_autoload_content($html, $id);
2475 }
2476
2477 // JQuery scripts for on-click content for elementor tab //
2478 $previeword = "Click to Load Panorama";
2479 if (isset($postdata['previewtext']) && $postdata['previewtext'] != '') {
2480 $previeword = $postdata['previewtext'];
2481 }
2482 $html = $this->render_elementor_tab_title_click_content($html, $id, $default_scene, $previeword);
2483 // JQuery scripts for on-click content for vr tour tab //
2484 $html = $this->render_vr_tour_tab_click_content($html, $id, $default_scene);
2485
2486 if (isset($previeword ) && $previeword != '') {
2487 $html .= 'jQuery("#pano' . $id . '").children(".pnlm-ui").find(".pnlm-load-button p").text("' . $previeword . '")';
2488 }
2489
2490 $html .= '});';
2491 $html .= '</script>';
2492 return $html;
2493 }
2494
2495
2496 /**
2497 * Return scripts content for hotspot data on shortcode
2498 *
2499 * @param mixed $html
2500 *
2501 * @return string
2502 * @since 8.0.0
2503 */
2504 private function get_shortcode_hotspot_script_content($html)
2505 {
2506 $html .= 'if(scenes) {';
2507 $html .= 'var scenedata = scenes.scenes;';
2508 $html .= 'for(var i in scenedata) {';
2509 $html .= 'var scenehotspot = scenedata[i].hotSpots;';
2510 $html .= 'for(var i = 0; i < scenehotspot.length; i++) {';
2511 $html .= 'if(scenehotspot[i]["clickHandlerArgs"] != "") {';
2512 $html .= 'scenehotspot[i]["clickHandlerFunc"] = wpvrhotspot;';
2513 $html .= '}';
2514 if (wpvr_isMobileDevice() && get_option('dis_on_hover') == "true") {
2515 } else {
2516 $html .= 'if(scenehotspot[i]["createTooltipArgs"] != "") {';
2517 $html .= 'scenehotspot[i]["createTooltipFunc"] = wpvrtooltip;';
2518 $html .= '}';
2519 }
2520 $html .= '}';
2521 $html .= '}';
2522 $html .= '}';
2523 return $html;
2524 }
2525
2526
2527 /**
2528 * Render scripts content for panaromic show on shortcode
2529 *
2530 * @param mixed $html
2531 * @param mixed $id
2532 *
2533 * @return string
2534 * @since 8.0.0
2535 */
2536 private function render_shortcode_panoshow_script_content($html, $id)
2537 {
2538 $html .= 'panoshow' . $id . '.on("load", function (){
2539 setTimeout(() => {
2540 window.dispatchEvent(new Event("resize"));
2541 }, 200);
2542 if (jQuery("#pano' . $id . '").children().children(".pnlm-panorama-info:visible").length > 0) {
2543 jQuery("#controls' . $id . '").css("bottom", "55px");
2544 }
2545 else {
2546 jQuery("#controls' . $id . '").css("bottom", "5px");
2547 }
2548 });';
2549 $html .= 'panoshow' . $id . '.on("render", function (){
2550 window.dispatchEvent(new Event("resize"));
2551 });';
2552 return $html;
2553 }
2554
2555
2556 /**
2557 * Return auto rotation scripts content on shortcode
2558 *
2559 * @param mixed $html
2560 * @param mixed $id
2561 *
2562 * @return string
2563 * @since 8.0.0
2564 */
2565 private function render_shortcode_autoRotate_content($html, $id)
2566 {
2567 $html .= 'if (scenes.autoRotate) {
2568 panoshow' . $id . '.on("load", function (){
2569 setTimeout(function(){ panoshow' . $id . '.startAutoRotate(scenes.autoRotate, 0); }, 3000);
2570 });
2571 panoshow' . $id . '.on("scenechange", function (){
2572 setTimeout(function(){ panoshow' . $id . '.startAutoRotate(scenes.autoRotate, 0); }, 3000);
2573 });
2574 }';
2575 return $html;
2576 }
2577
2578
2579 /**
2580 * Return autoload scripts content on shortcode
2581 *
2582 * @param mixed $html
2583 * @param mixed $id
2584 *
2585 * @return string
2586 * @since 8.0.0
2587 */
2588 private function render_shortcode_autoload_content($html, $id)
2589 {
2590 $html .= 'jQuery(document).ready(function(){
2591 jQuery("#controls' . $id . '").hide();
2592 jQuery("#zoom-in-out-controls' . $id . '").hide();
2593 jQuery("#adcontrol' . $id . '").hide();
2594 jQuery("#pano' . $id . '").find(".pnlm-panorama-info").hide();
2595 });';
2596
2597 $html .= 'panoshow' . $id . '.on("load", function (){
2598 jQuery("#controls' . $id . '").show();
2599 jQuery("#zoom-in-out-controls' . $id . '").show();
2600 jQuery("#adcontrol' . $id . '").show();
2601 jQuery("#pano' . $id . '").find(".pnlm-panorama-info").show();
2602 });';
2603 return $html;
2604 }
2605
2606
2607 /**
2608 * JQuery scripts for on-click content for elementor tab
2609 *
2610 * @param mixed $html
2611 * @param mixed $id
2612 * @param mixed $default_scene
2613 *
2614 * @return string
2615 * @since 8.0.0
2616 */
2617 private function render_elementor_tab_title_click_content($html, $id, $default_scene, $previeword)
2618 {
2619 $html .= 'jQuery(".elementor-tab-title").click(function(){
2620 var element_id;
2621 var pano_id;
2622 var element_id = this.id;
2623 element_id = element_id.split("-");
2624 element_id = element_id[3];
2625 jQuery("#elementor-tab-content-"+element_id).children("div").addClass("awwww");
2626 var pano_id = jQuery(".awwww").attr("id");
2627 jQuery("#elementor-tab-content-"+element_id).children("div").removeClass("awwww");
2628 if (pano_id != undefined) {
2629 pano_id = pano_id.split("o");
2630 pano_id = pano_id[1];
2631 if (pano_id == "' . $id . '") {
2632 jQuery("#pano' . $id . '").children(".pnlm-render-container").remove();
2633 jQuery("#pano' . $id . '").children(".pnlm-ui").remove();
2634 panoshow' . $id . ' = pannellum.viewer(response[0]["panoid"], scenes);
2635 jQuery("#pano' . $id . '").children(".pnlm-ui").find(".pnlm-load-button p").text("' . $previeword . '")
2636 setTimeout(function() {
2637 // panoshow' . $id . '.loadScene("' . $default_scene . '");
2638 window.dispatchEvent(new Event("resize"));
2639 if (jQuery("#pano' . $id . '").children().children(".pnlm-panorama-info:visible").length > 0) {
2640 jQuery("#controls' . $id . '").css("bottom", "55px");
2641 }
2642 else {
2643 jQuery("#controls' . $id . '").css("bottom", "5px");
2644 }
2645 }, 200);
2646 }
2647 }
2648 });';
2649 return $html;
2650 }
2651
2652
2653 /**
2654 * JQuery scripts for on-click content for vr tour tab
2655 *
2656 * @param mixed $html
2657 * @param mixed $id
2658 * @param mixed $default_scene
2659 *
2660 * @return string
2661 * @since 8.0.0
2662 */
2663 private function render_vr_tour_tab_click_content($html, $id, $default_scene)
2664 {
2665 $html .= 'jQuery(".geodir-tab-head dd, #vr-tour-tab").click(function(){
2666 jQuery("#pano' . $id . '").children(".pnlm-render-container").remove();
2667 jQuery("#pano' . $id . '").children(".pnlm-ui").remove();
2668 panoshow' . $id . ' = pannellum.viewer(response[0]["panoid"], scenes);
2669 setTimeout(function() {
2670 panoshow' . $id . '.loadScene("' . $default_scene . '");
2671 window.dispatchEvent(new Event("resize"));
2672 if (jQuery("#pano' . $id . '").children().children(".pnlm-panorama-info:visible").length > 0) {
2673 jQuery("#controls' . $id . '").css("bottom", "55px");
2674 }
2675 else {
2676 jQuery("#controls' . $id . '").css("bottom", "5px");
2677 }
2678 }, 200);
2679 });';
2680 return $html;
2681 }
2682 }
2683