PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.72
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.72
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.72, at admin/helpers/class-wpvr-format.php

2,565 lines 111.9 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_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-' . 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 update360DragAvailability() {
660 var sphericalView = getSphericalView();
661 setDragSurfaceEnabled(!!sphericalView);
662 }
663
664 function schedule360AvailabilityChecks() {
665 update360DragAvailability();
666 window.setTimeout(update360DragAvailability, 500);
667 window.setTimeout(update360DragAvailability, 1500);
668 window.setTimeout(update360DragAvailability, 3000);
669 }
670
671 if (dragSurface) {
672 dragSurface.addEventListener("mousedown", function(event) {
673 var sphericalView = getSphericalView();
674 if (!sphericalView) return;
675 event.preventDefault();
676 dragStartX = event.clientX;
677 dragStartY = event.clientY;
678 dragStartView = sphericalView;
679 dragSurface.style.cursor = "grabbing";
680 });
681
682 dragSurface.addEventListener("dragstart", function(event) { event.preventDefault(); });
683
684 window.addEventListener("mousemove", function(event) {
685 var nextView, deltaX, deltaY;
686 if (!isDragging && dragStartView) {
687 deltaX = event.clientX - dragStartX;
688 deltaY = event.clientY - dragStartY;
689 if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) > 5) isDragging = true;
690 }
691 if (!isDragging || !dragStartView || !ytPlayer || typeof ytPlayer.setSphericalProperties !== "function") return;
692 event.preventDefault();
693 deltaX = event.clientX - dragStartX;
694 deltaY = event.clientY - dragStartY;
695 nextView = {
696 yaw: (dragStartView.yaw || 0) + (deltaX * 0.2),
697 pitch: clamp((dragStartView.pitch || 0) + (deltaY * 0.2), -85, 85),
698 roll: dragStartView.roll || 0,
699 fov: dragStartView.fov || 100
700 };
701 ytPlayer.setSphericalProperties(nextView);
702 });
703
704 window.addEventListener("mouseup", function() {
705 if (!isDragging && dragStartView && ytPlayer) {
706 var playerState = ytPlayer.getPlayerState();
707 if (playerState === 1) ytPlayer.pauseVideo();
708 else ytPlayer.playVideo();
709 }
710 isDragging = false;
711 dragStartView = null;
712 if (dragSurface) dragSurface.style.cursor = "grab";
713 });
714 }
715
716 function wpvrLoadYouTubeApi(callback) {
717 window.wpvrYoutubeApiCallbacks = window.wpvrYoutubeApiCallbacks || [];
718 if (window.YT && window.YT.Player) { callback(); return; }
719 window.wpvrYoutubeApiCallbacks.push(callback);
720 if (window.wpvrYoutubeApiLoading) return;
721 window.wpvrYoutubeApiLoading = true;
722 var previousReady = window.onYouTubeIframeAPIReady;
723 window.onYouTubeIframeAPIReady = function() {
724 if (typeof previousReady === "function") previousReady();
725 var callbacks = window.wpvrYoutubeApiCallbacks || [];
726 while (callbacks.length) {
727 var queuedCallback = callbacks.shift();
728 if (typeof queuedCallback === "function") queuedCallback();
729 }
730 };
731 var tag = document.createElement("script");
732 tag.src = "https://www.youtube.com/iframe_api";
733 var firstScriptTag = document.getElementsByTagName("script")[0];
734 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
735 }
736
737 function initializeYouTubePlayer() {
738 if (playerInitialised) return;
739 wpvrLoadYouTubeApi(function() {
740 if (playerInitialised || !(window.YT && window.YT.Player)) return;
741 ytPlayer = new YT.Player("' . esc_js( $preview_id . '-iframe' ) . '", {
742 events: {
743 onReady: function() {
744 schedule360AvailabilityChecks();
745 startProgressLoop();
746 },
747 onStateChange: function() {
748 schedule360AvailabilityChecks();
749 startProgressLoop();
750 }
751 }
752 });
753 playerInitialised = true;
754 });
755 }
756
757 // === Custom control bar logic ===
758 var outerContainer = document.getElementById("' . esc_js( $preview_id ) . '-container");
759 var controlBar = document.getElementById("' . esc_js( $preview_id ) . '-controls");
760 var playBtn = document.getElementById("' . esc_js( $preview_id ) . '-play-btn");
761 var playIcon = document.getElementById("' . esc_js( $preview_id ) . '-play-icon");
762 var timeCurrent = document.getElementById("' . esc_js( $preview_id ) . '-time-current");
763 var timeDuration = document.getElementById("' . esc_js( $preview_id ) . '-time-duration");
764 var progressWrap = document.getElementById("' . esc_js( $preview_id ) . '-progress-wrap");
765 var progressBar = document.getElementById("' . esc_js( $preview_id ) . '-progress-bar");
766 var progressBuffered = document.getElementById("' . esc_js( $preview_id ) . '-progress-buffered");
767 var progressThumb = document.getElementById("' . esc_js( $preview_id ) . '-progress-thumb");
768 var fsBtn = document.getElementById("' . esc_js( $preview_id ) . '-fs-btn");
769 var frameEl = document.getElementById("' . esc_js( $preview_id ) . '-frame");
770 var muteBtn = document.getElementById("' . esc_js( $preview_id ) . '-mute-btn");
771 var volSlider = document.getElementById("' . esc_js( $preview_id ) . '-vol-slider");
772 var volSliderWrap = document.getElementById("' . esc_js( $preview_id ) . '-vol-slider-wrap");
773 var volIcon = document.getElementById("' . esc_js( $preview_id ) . '-vol-icon");
774
775 var playPath = "M8 5v14l11-7z";
776 var pausePath = "M6 19h4V5H6v14zm8-14v14h4V5h-4z";
777 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";
778 var volIconLow = "M7 9v6h4l5 5V4L7 9z";
779 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";
780 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";
781
782 function formatTime(sec) {
783 if (!sec || isNaN(sec)) return "0:00";
784 sec = Math.floor(sec);
785 var m = Math.floor(sec / 60);
786 var s = sec % 60;
787 return m + ":" + (s < 10 ? "0" : "") + s;
788 }
789
790 if (controlBar) controlBar.style.display = "flex";
791
792 // --- Play / Pause ---
793 if (playBtn) {
794 playBtn.addEventListener("click", function() {
795 if (!ytPlayer) return;
796 var state = ytPlayer.getPlayerState();
797 if (state === 1) ytPlayer.pauseVideo();
798 else ytPlayer.playVideo();
799 });
800 }
801
802 function updatePlayIcon() {
803 if (!ytPlayer || !playIcon) return;
804 try {
805 var state = ytPlayer.getPlayerState();
806 playIcon.querySelector("path").setAttribute("d", state === 1 ? pausePath : playPath);
807 } catch(e) {}
808 }
809
810 // --- Progress bar ---
811 var progressAnimId = null;
812 var isSeeking = false;
813 var progressLoopStarted = false;
814
815 function startProgressLoop() {
816 if (progressLoopStarted) return;
817 progressLoopStarted = true;
818 progressAnimId = requestAnimationFrame(updateProgress);
819 }
820
821 function updateProgress() {
822 if (!ytPlayer || isSeeking) { progressAnimId = requestAnimationFrame(updateProgress); return; }
823 try {
824 var current = typeof ytPlayer.getCurrentTime === "function" ? ytPlayer.getCurrentTime() : 0;
825 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
826 if (duration > 0) {
827 var pct = (current / duration) * 100;
828 progressBar.style.width = pct + "%";
829 progressThumb.style.left = pct + "%";
830 timeCurrent.textContent = formatTime(current);
831 timeDuration.textContent = formatTime(duration);
832 }
833 if (typeof ytPlayer.getVideoLoadedFraction === "function") {
834 progressBuffered.style.width = (ytPlayer.getVideoLoadedFraction() * 100) + "%";
835 }
836 updatePlayIcon();
837 } catch (e) {}
838 progressAnimId = requestAnimationFrame(updateProgress);
839 }
840
841 if (progressWrap) {
842 function seekFromEvent(e) {
843 var rect = progressWrap.getBoundingClientRect();
844 var x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
845 var pct = x / rect.width;
846 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
847 if (duration > 0 && ytPlayer) {
848 ytPlayer.seekTo(pct * duration, true);
849 progressBar.style.width = (pct * 100) + "%";
850 progressThumb.style.left = (pct * 100) + "%";
851 timeCurrent.textContent = formatTime(pct * duration);
852 }
853 }
854 progressWrap.addEventListener("mousedown", function(e) {
855 isSeeking = true;
856 seekFromEvent(e);
857 function onMove(e) { seekFromEvent(e); }
858 function onUp() {
859 isSeeking = false;
860 document.removeEventListener("mousemove", onMove);
861 document.removeEventListener("mouseup", onUp);
862 }
863 document.addEventListener("mousemove", onMove);
864 document.addEventListener("mouseup", onUp);
865 });
866 progressWrap.addEventListener("mouseenter", function() { progressThumb.style.display = "block"; });
867 progressWrap.addEventListener("mouseleave", function() { if (!isSeeking) progressThumb.style.display = "none"; });
868 }
869
870 // --- Volume ---
871 var volHideTimer = null;
872 var localVol = 100;
873 var localMuted = false;
874
875 function updateVolIconFromLocal() {
876 if (!volIcon) return;
877 var path;
878 if (localMuted || localVol === 0) path = volIconMuted;
879 else if (localVol < 40) path = volIconLow;
880 else if (localVol < 75) path = volIconMed;
881 else path = volIconHigh;
882 volIcon.querySelector("path").setAttribute("d", path);
883 }
884
885 function updateVolIcon() { updateVolIconFromLocal(); }
886
887 function showVolSlider() {
888 if (volHideTimer) { clearTimeout(volHideTimer); volHideTimer = null; }
889 if (volSliderWrap) volSliderWrap.style.display = "flex";
890 }
891 function hideVolSlider() {
892 volHideTimer = setTimeout(function() { if (volSliderWrap) volSliderWrap.style.display = "none"; }, 400);
893 }
894
895 if (muteBtn) {
896 muteBtn.addEventListener("click", function() {
897 if (!ytPlayer) return;
898 if (localMuted) {
899 ytPlayer.unMute(); localMuted = false;
900 if (localVol === 0) { localVol = 100; ytPlayer.setVolume(100); }
901 if (volSlider) volSlider.value = localVol;
902 } else {
903 ytPlayer.mute(); localMuted = true;
904 if (volSlider) volSlider.value = 0;
905 }
906 updateVolIconFromLocal();
907 updateVolSliderFill();
908 });
909 muteBtn.addEventListener("mouseenter", showVolSlider);
910 muteBtn.addEventListener("mouseleave", hideVolSlider);
911 }
912 if (volSliderWrap) {
913 volSliderWrap.addEventListener("mouseenter", showVolSlider);
914 volSliderWrap.addEventListener("mouseleave", hideVolSlider);
915 }
916 function updateVolSliderFill() {
917 if (!volSlider) return;
918 var v = parseInt(volSlider.value, 10);
919 volSlider.style.background = "linear-gradient(to right,#fff " + v + "%,rgba(255,255,255,0.3) " + v + "%)";
920 }
921 if (volSlider) {
922 volSlider.addEventListener("input", function() {
923 if (!ytPlayer) return;
924 var val = parseInt(volSlider.value, 10);
925 localVol = val;
926 ytPlayer.setVolume(val);
927 if (val === 0) { ytPlayer.mute(); localMuted = true; }
928 else if (localMuted) { ytPlayer.unMute(); localMuted = false; }
929 updateVolIconFromLocal();
930 updateVolSliderFill();
931 });
932 updateVolSliderFill();
933 }
934
935 // --- Fullscreen ---
936 if (outerContainer) {
937 var savedFrameWidth = frameEl ? frameEl.style.width : "";
938 var savedFrameHeight = frameEl ? frameEl.style.height : "";
939 var savedFrameMaxWidth = frameEl ? frameEl.style.maxWidth : "";
940 var savedContainerWidth = outerContainer.style.width;
941 var savedContainerHeight = outerContainer.style.height;
942 var savedContainerBackground = outerContainer.style.background;
943
944 if(fsBtn){
945 fsBtn.addEventListener("click", function() {
946 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
947 if (fsEl) {
948 (document.exitFullscreen || document.webkitExitFullscreen).call(document);
949 } else {
950 (outerContainer.requestFullscreen || outerContainer.webkitRequestFullscreen).call(outerContainer);
951 }
952 });
953 }
954
955 function onFsChange() {
956 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
957 var isFs = (fsEl === outerContainer);
958 frameEl.style.width = isFs ? "100%" : savedFrameWidth;
959 frameEl.style.height = isFs ? "100%" : savedFrameHeight;
960 frameEl.style.maxWidth = isFs ? "100%" : savedFrameMaxWidth;
961 outerContainer.style.width = isFs ? "100%" : savedContainerWidth;
962 outerContainer.style.height = isFs ? "100%" : savedContainerHeight;
963 outerContainer.style.background = isFs ? "#000" : savedContainerBackground;
964 fsBtn.querySelector("svg path").setAttribute("d", isFs
965 ? "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"
966 : "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
967 );
968 schedule360AvailabilityChecks();
969 }
970 document.addEventListener("fullscreenchange", onFsChange);
971 document.addEventListener("webkitfullscreenchange", onFsChange);
972 }
973
974 // Sync controls after player ready
975 var origSchedule = schedule360AvailabilityChecks;
976 schedule360AvailabilityChecks = function() {
977 origSchedule();
978 updateVolIcon();
979 updatePlayIcon();
980 };
981
982 // Initialize immediately for admin preview
983 initializeYouTubePlayer();
984 })();
985 </script>';
986
987 return $html;
988 }
989
990
991 /**
992 * Extract YouTube video ID from various URL formats.
993 *
994 * Handles youtube.com/watch?v=, youtu.be/, youtube.com/embed/, and youtube.com/v/ formats.
995 * Strips extra query parameters and sanitizes the ID to only valid YouTube characters.
996 *
997 * @param string $url The YouTube video URL.
998 *
999 * @return string The sanitized YouTube video ID, or empty string on failure.
1000 * @since 8.0.0
1001 */
1002 private function extract_youtube_video_id( $url ) {
1003 $url = trim( $url );
1004 $parsed = wp_parse_url( $url );
1005
1006 // Handle youtu.be short URLs.
1007 if ( isset( $parsed['host'] ) && 'youtu.be' === $parsed['host'] ) {
1008 $video_id = isset( $parsed['path'] ) ? ltrim( $parsed['path'], '/' ) : '';
1009 return preg_replace( '/[^A-Za-z0-9_\-]/', '', $video_id );
1010 }
1011
1012 // Handle youtube.com/embed/VIDEO_ID or youtube.com/v/VIDEO_ID.
1013 if ( isset( $parsed['path'] ) && preg_match( '#/(embed|v)/([A-Za-z0-9_\-]+)#', $parsed['path'], $matches ) ) {
1014 return $matches[2];
1015 }
1016
1017 // Handle youtube.com/watch?v=VIDEO_ID (standard format).
1018 if ( isset( $parsed['query'] ) ) {
1019 parse_str( $parsed['query'], $query_params );
1020 if ( ! empty( $query_params['v'] ) ) {
1021 return preg_replace( '/[^A-Za-z0-9_\-]/', '', $query_params['v'] );
1022 }
1023 }
1024
1025 // Fallback: try naive extraction for edge cases.
1026 if ( preg_match( '/[?&]v=([A-Za-z0-9_\-]+)/', $url, $matches ) ) {
1027 return $matches[1];
1028 }
1029
1030 return '';
1031 }
1032
1033
1034 /**
1035 * Prepare shortcode data for youtube video url
1036 *
1037 * @param array $postdata
1038 * @param mixed $width
1039 * @param mixed $height
1040 *
1041 * @return string
1042 * @since 8.0.0
1043 */
1044 public function prepare_youtube_video_shortcode_data($postdata, $width, $height, $autoplay, $loop, $radius)
1045 {
1046 $muted = ($autoplay == 'on') ? '&mute=1' : '';
1047 $autoplay = ($autoplay == 'on') ? '&autoplay=1' : '';
1048 $loop = ($loop == 'on') ? '&loop=1' : '';
1049 $origin = '&origin=' . rawurlencode( home_url() );
1050
1051 $expdata = $this->extract_youtube_video_id( $postdata['vidurl'] );
1052
1053 $playlist = '&playlist=' . $expdata;
1054
1055 $html = '';
1056 $html .= '<div style="text-align:center; max-width:100%; width:' . $width . '; height:' . $height . '; border-radius: ' . $radius . '; margin: 0 auto;">';
1057
1058 // Add browser compatibility detection script
1059 $html .= '<script>
1060 function wpvr_check_360_support() {
1061 var support = {
1062 supported: false,
1063 fullySupported: false,
1064 webgl: false,
1065 orientation: false,
1066 gyroscope: false,
1067 touch: false,
1068 browser: "unknown",
1069 isMobile: false,
1070 details: []
1071 };
1072
1073 // Detect mobile devices
1074 support.isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
1075 if (support.isMobile) {
1076 support.details.push("Mobile device detected");
1077 }
1078
1079 // Browser detection
1080 var ua = navigator.userAgent;
1081 if (/^((?!chrome|android).)*safari/i.test(ua)) {
1082 support.browser = "safari";
1083 support.details.push("Safari browser detected");
1084 } else if (ua.indexOf("Chrome") > -1) {
1085 support.browser = "chrome";
1086 support.details.push("Chrome browser detected");
1087 } else if (ua.indexOf("Firefox") > -1) {
1088 support.browser = "firefox";
1089 support.details.push("Firefox browser detected");
1090 } else if (ua.indexOf("MSIE") > -1 || ua.indexOf("Trident") > -1) {
1091 support.browser = "ie";
1092 support.details.push("Internet Explorer detected");
1093 } else if (ua.indexOf("Edge") > -1 || ua.indexOf("Edg") > -1) {
1094 support.browser = "edge";
1095 support.details.push("Edge browser detected");
1096 } else if (ua.indexOf("Opera") > -1 || ua.indexOf("OPR") > -1) {
1097 support.browser = "opera";
1098 support.details.push("Opera browser detected");
1099 }
1100
1101 // Check WebGL support
1102 try {
1103 var canvas = document.createElement("canvas");
1104 support.webgl = !!(window.WebGLRenderingContext &&
1105 (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")));
1106
1107 if (support.webgl) {
1108 support.details.push("WebGL supported");
1109 } else {
1110 support.details.push("WebGL not supported");
1111 }
1112 } catch (e) {
1113 support.webgl = false;
1114 support.details.push("WebGL detection error: " + e.message);
1115 }
1116
1117 // Check device orientation support
1118 support.orientation = !!(window.DeviceOrientationEvent);
1119 if (support.orientation) {
1120 support.details.push("Device Orientation API supported");
1121 } else {
1122 support.details.push("Device Orientation API not supported");
1123 }
1124
1125 // Check for gyroscope
1126 if (window.Gyroscope) {
1127 support.gyroscope = true;
1128 support.details.push("Gyroscope supported");
1129 } else {
1130 support.details.push("Gyroscope not supported");
1131 }
1132
1133 // Check touch support for mobile interaction
1134 support.touch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
1135 if (support.touch) {
1136 support.details.push("Touch supported");
1137 } else {
1138 support.details.push("Touch not supported");
1139 }
1140
1141 // Overall support determination
1142 support.supported = support.webgl;
1143
1144 // Full support means WebGL + orientation on mobile or WebGL on desktop
1145 support.fullySupported = support.webgl &&
1146 ((support.isMobile && support.orientation) || !support.isMobile);
1147
1148 // Browser-specific warnings
1149 if (support.browser === "safari" && /iPhone|iPad|iPod/.test(navigator.userAgent)) {
1150 support.browserWarning = "iPhone has limited support for 360 videos in browser. The experience may not be optimal.";
1151 support.fullySupported = false;
1152 } else if (support.browser === "ie") {
1153 support.browserWarning = "Internet Explorer has limited support for 360 videos. The experience may not be optimal.";
1154 support.fullySupported = false;
1155 } else if (!support.supported) {
1156 support.browserWarning = "Your browser does not fully support 360° videos. For the best experience, please use a modern browser with WebGL support.";
1157 }
1158
1159 return support;
1160 }
1161 </script>';
1162
1163 $random_id = 'video-container-' . rand(10000, 99999);
1164 $html .= '<div id="' . $random_id . '-container" style="position:relative; width:100%; height:100%;">';
1165
1166 // First, always check browser compatibility before showing anything
1167 $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 . ';">
1168 <div style="margin-bottom:20px; text-align:center;">
1169 <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>
1170 <p style="margin:0;">Checking browser compatibility...</p>
1171 </div>
1172 </div>';
1173
1174 // Add animation for spinner
1175 $html .= '<style>@keyframes wpvr-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>';
1176
1177 // Add the iframe (visible by default so YouTube 360° WebGL context gets correct dimensions)
1178 $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;">';
1179 $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>';
1180 $html .= '</div>';
1181 $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>';
1182
1183 // Custom control bar
1184 $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;">';
1185
1186 // Play/Pause button
1187 $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">';
1188 $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>';
1189 $html .= '</button>';
1190
1191 // Time current
1192 $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>';
1193
1194 // Progress bar
1195 $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;">';
1196 $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>';
1197 $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>';
1198 $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>';
1199 $html .= '</div>';
1200
1201 // Time duration
1202 $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>';
1203
1204 // Mute button
1205 $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">';
1206 $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>';
1207 $html .= '</button>';
1208
1209 // Volume slider (popup on mute hover)
1210 $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>';
1211 $html .= '<div id="' . esc_attr( $random_id . '-vol-slider-wrap' ) . '" style="display:none; align-items:center; height:40px; padding:0px 4px;">';
1212 $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;" />';
1213 $html .= '</div>';
1214
1215 // Fullscreen button
1216 $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">';
1217 $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>';
1218 $html .= '</button>';
1219
1220 $html .= '</div>'; // Close controls
1221
1222 // Add permission request button (hidden by default) - Only shown for fully supported devices
1223 $html .= '<div id="' . $random_id . '-permission-request" 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 . ';">';
1224 $html .= '<p style="font-size:16px; margin:0 20px 15px;">For the best 360° video experience on mobile</p>';
1225 $html .= '<button id="' . $random_id . '-permission-button" style="padding:10px 15px; background-color:#0085ba; color:#fff; border:none; border-radius:4px; cursor:pointer;">Allow motion and orientation access</button>';
1226 $html .= '</div>';
1227
1228 // Add browser compatibility warning message
1229 $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 . ';">';
1230 $html .= '<p id="' . $random_id . '-warning-text" style="font-size:16px; margin:0 20px 5px;"></p>';
1231 $html .= '<p style="font-size:14px; margin:5px 20px 15px;">For the best experience, consider using Chrome or Firefox.</p>';
1232 $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>';
1233 $html .= '</div>';
1234
1235 // Main script for handling compatibility and permissions
1236 $html .= '<script>
1237 // Wait for DOM to be fully loaded
1238 document.addEventListener("DOMContentLoaded", function() {
1239 var playerInitialised = false;
1240 var ytPlayer = null;
1241 var dragSurface = document.getElementById("' . $random_id . '-drag-surface");
1242 var isDragging = false;
1243 var dragStartX = 0;
1244 var dragStartY = 0;
1245 var dragStartView = null;
1246
1247 function clamp(value, min, max) {
1248 return Math.min(Math.max(value, min), max);
1249 }
1250
1251 function getSphericalView() {
1252 if (!ytPlayer || typeof ytPlayer.getSphericalProperties !== "function") {
1253 return null;
1254 }
1255
1256 var view = ytPlayer.getSphericalProperties();
1257 return view && Object.keys(view).length ? view : null;
1258 }
1259
1260 function setDragSurfaceEnabled(enabled) {
1261 if (!dragSurface) {
1262 return;
1263 }
1264
1265 dragSurface.style.display = enabled ? "block" : "none";
1266 dragSurface.style.pointerEvents = enabled ? "auto" : "none";
1267 }
1268
1269 function update360DragAvailability() {
1270 var sphericalView = getSphericalView();
1271 setDragSurfaceEnabled(!!sphericalView && !supportInfo.isMobile);
1272 }
1273
1274 function schedule360AvailabilityChecks() {
1275 update360DragAvailability();
1276 window.setTimeout(update360DragAvailability, 500);
1277 window.setTimeout(update360DragAvailability, 1500);
1278 window.setTimeout(update360DragAvailability, 3000);
1279 }
1280
1281 if (dragSurface) {
1282 dragSurface.addEventListener("mousedown", function(event) {
1283 var sphericalView = getSphericalView();
1284 if (!sphericalView) {
1285 return;
1286 }
1287
1288 event.preventDefault();
1289
1290 dragStartX = event.clientX;
1291 dragStartY = event.clientY;
1292 dragStartView = sphericalView;
1293 dragSurface.style.cursor = "grabbing";
1294 });
1295
1296 dragSurface.addEventListener("dragstart", function(event) {
1297 event.preventDefault();
1298 });
1299
1300 window.addEventListener("mousemove", function(event) {
1301 var nextView;
1302 var deltaX;
1303 var deltaY;
1304
1305 // Activate drag only after moving more than 5px
1306 if (!isDragging && dragStartView) {
1307 deltaX = event.clientX - dragStartX;
1308 deltaY = event.clientY - dragStartY;
1309 if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) > 5) {
1310 isDragging = true;
1311 }
1312 }
1313
1314 if (!isDragging || !dragStartView || !ytPlayer || typeof ytPlayer.setSphericalProperties !== "function") {
1315 return;
1316 }
1317
1318 event.preventDefault();
1319 deltaX = event.clientX - dragStartX;
1320 deltaY = event.clientY - dragStartY;
1321
1322 nextView = {
1323 yaw: (dragStartView.yaw || 0) + (deltaX * 0.2),
1324 pitch: clamp((dragStartView.pitch || 0) + (deltaY * 0.2), -85, 85),
1325 roll: dragStartView.roll || 0,
1326 fov: dragStartView.fov || 100
1327 };
1328
1329 ytPlayer.setSphericalProperties(nextView);
1330 });
1331
1332 window.addEventListener("mouseup", function() {
1333 if (!isDragging && dragStartView && ytPlayer) {
1334 var playerState = ytPlayer.getPlayerState();
1335 if (playerState === 1) { // YT.PlayerState.PLAYING
1336 ytPlayer.pauseVideo();
1337 } else {
1338 ytPlayer.playVideo();
1339 }
1340 }
1341 isDragging = false;
1342 dragStartView = null;
1343 if (dragSurface) {
1344 dragSurface.style.cursor = "grab";
1345 }
1346 });
1347 }
1348
1349 function wpvrLoadYouTubeApi(callback) {
1350 window.wpvrYoutubeApiCallbacks = window.wpvrYoutubeApiCallbacks || [];
1351
1352 if (window.YT && window.YT.Player) {
1353 callback();
1354 return;
1355 }
1356
1357 window.wpvrYoutubeApiCallbacks.push(callback);
1358
1359 if (window.wpvrYoutubeApiLoading) {
1360 return;
1361 }
1362
1363 window.wpvrYoutubeApiLoading = true;
1364
1365 var previousReady = window.onYouTubeIframeAPIReady;
1366 window.onYouTubeIframeAPIReady = function() {
1367 if (typeof previousReady === "function") {
1368 previousReady();
1369 }
1370
1371 var callbacks = window.wpvrYoutubeApiCallbacks || [];
1372 while (callbacks.length) {
1373 var queuedCallback = callbacks.shift();
1374 if (typeof queuedCallback === "function") {
1375 queuedCallback();
1376 }
1377 }
1378 };
1379
1380 var tag = document.createElement("script");
1381 tag.src = "https://www.youtube.com/iframe_api";
1382 var firstScriptTag = document.getElementsByTagName("script")[0];
1383 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
1384 }
1385
1386 function initializeYouTubePlayer() {
1387 if (playerInitialised) {
1388 return;
1389 }
1390
1391 wpvrLoadYouTubeApi(function() {
1392 if (playerInitialised || !(window.YT && window.YT.Player)) {
1393 return;
1394 }
1395
1396 ytPlayer = new YT.Player("' . esc_js( $random_id . '-iframe' ) . '", {
1397 events: {
1398 onReady: function() {
1399 schedule360AvailabilityChecks();
1400 startProgressLoop();
1401 },
1402 onStateChange: function() {
1403 schedule360AvailabilityChecks();
1404 startProgressLoop();
1405 }
1406 }
1407 });
1408 playerInitialised = true;
1409 });
1410 }
1411
1412 // Check compatibility first
1413 var supportInfo = wpvr_check_360_support();
1414 let compatibilityCheck = document.getElementById("' . $random_id . '-compatibility-check");
1415 if(supportInfo.isMobile && !supportInfo.fullySupported) {
1416 compatibilityCheck.style.display = "flex";
1417 compatibilityCheck.style.pointerEvents = "auto";
1418 }
1419 // Check browser compatibility and handle overlays accordingly.
1420 // The iframe is already visible by default so the YouTube 360° WebGL
1421 // renderer gets the correct dimensions during initialisation.
1422 setTimeout(function() {
1423 document.getElementById("' . $random_id . '-compatibility-check").style.display = "none";
1424 document.getElementById("' . $random_id . '-compatibility-check").style.pointerEvents = "none";
1425 // If browser doesn\'t support 360 videos well, hide the iframe and show warning
1426 if (!supportInfo.supported || supportInfo.browserWarning) {
1427 document.getElementById("' . $random_id . '-frame").style.display = "none";
1428 document.getElementById("' . $random_id . '-warning-text").textContent =
1429 supportInfo.browserWarning || "Your browser may not fully support 360° videos.";
1430 var warningEl = document.getElementById("' . $random_id . '-browser-warning");
1431 warningEl.style.display = "flex";
1432 warningEl.style.pointerEvents = "auto";
1433
1434 // Add continue anyway button handler
1435 document.getElementById("' . $random_id . '-browser-continue").addEventListener("click", function() {
1436 warningEl.style.display = "none";
1437 warningEl.style.pointerEvents = "none";
1438 document.getElementById("' . $random_id . '-frame").style.display = "block";
1439 });
1440 }
1441 // If browser fully supports 360 and is mobile, hide the iframe and show permission request
1442 else if (supportInfo.fullySupported && supportInfo.isMobile) {
1443 document.getElementById("' . $random_id . '-frame").style.display = "none";
1444 var permEl = document.getElementById("' . $random_id . '-permission-request");
1445 permEl.style.display = "flex";
1446 permEl.style.pointerEvents = "auto";
1447
1448 // Add permission request button handler
1449 document.getElementById("' . $random_id . '-permission-button").addEventListener("click", function() {
1450 requestDevicePermissions();
1451 });
1452 }
1453 // Otherwise (supported desktop browser): iframe is already visible, nothing to do.
1454 else {
1455 initializeYouTubePlayer();
1456 }
1457 }, 500);
1458
1459 // Function to request device permissions on supported mobile devices
1460 function requestDevicePermissions() {
1461 try {
1462 if (typeof DeviceOrientationEvent !== "undefined" &&
1463 typeof DeviceOrientationEvent.requestPermission === "function") {
1464
1465 DeviceOrientationEvent.requestPermission()
1466 .then(function(response) {
1467 if (response === "granted") {
1468 // Also request motion permission if available
1469 if (typeof DeviceMotionEvent !== "undefined" &&
1470 typeof DeviceMotionEvent.requestPermission === "function") {
1471
1472 DeviceMotionEvent.requestPermission()
1473 .then(function() {
1474 showVideo();
1475 })
1476 .catch(function() {
1477 showVideo();
1478 });
1479 } else {
1480 showVideo();
1481 }
1482 } else {
1483 showVideo();
1484 }
1485 })
1486 .catch(function() {
1487 showVideo();
1488 });
1489 } else {
1490 showVideo();
1491 }
1492 } catch (error) {
1493 showVideo();
1494 }
1495 }
1496
1497 // Helper function to show video after permissions
1498 function showVideo() {
1499 var permReq = document.getElementById("' . $random_id . '-permission-request");
1500 permReq.style.display = "none";
1501 permReq.style.pointerEvents = "none";
1502 document.getElementById("' . $random_id . '-frame").style.display = "block";
1503 initializeYouTubePlayer();
1504 }
1505
1506 // === Custom control bar logic ===
1507 var outerContainer = document.getElementById("' . $random_id . '-container");
1508 var controlBar = document.getElementById("' . $random_id . '-controls");
1509 var playBtn = document.getElementById("' . $random_id . '-play-btn");
1510 var playIcon = document.getElementById("' . $random_id . '-play-icon");
1511 var timeCurrent = document.getElementById("' . $random_id . '-time-current");
1512 var timeDuration = document.getElementById("' . $random_id . '-time-duration");
1513 var progressWrap = document.getElementById("' . $random_id . '-progress-wrap");
1514 var progressBar = document.getElementById("' . $random_id . '-progress-bar");
1515 var progressBuffered = document.getElementById("' . $random_id . '-progress-buffered");
1516 var progressThumb = document.getElementById("' . $random_id . '-progress-thumb");
1517 var fsBtn = document.getElementById("' . $random_id . '-fs-btn");
1518 var frameEl = document.getElementById("' . $random_id . '-frame");
1519 var muteBtn = document.getElementById("' . $random_id . '-mute-btn");
1520 var volSlider = document.getElementById("' . $random_id . '-vol-slider");
1521 var volSliderWrap = document.getElementById("' . $random_id . '-vol-slider-wrap");
1522 var volIcon = document.getElementById("' . $random_id . '-vol-icon");
1523
1524 var playPath = "M8 5v14l11-7z";
1525 var pausePath = "M6 19h4V5H6v14zm8-14v14h4V5h-4z";
1526 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";
1527 var volIconLow = "M7 9v6h4l5 5V4L7 9z";
1528 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";
1529 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";
1530
1531 function formatTime(sec) {
1532 if (!sec || isNaN(sec)) return "0:00";
1533 sec = Math.floor(sec);
1534 var m = Math.floor(sec / 60);
1535 var s = sec % 60;
1536 return m + ":" + (s < 10 ? "0" : "") + s;
1537 }
1538
1539 // Show control bar once player exists
1540 if (controlBar) controlBar.style.display = "flex";
1541
1542 // --- Play / Pause ---
1543 if (playBtn) {
1544 playBtn.addEventListener("click", function() {
1545 if (!ytPlayer) return;
1546 var state = ytPlayer.getPlayerState();
1547 if (state === 1) { ytPlayer.pauseVideo(); }
1548 else { ytPlayer.playVideo(); }
1549 });
1550 }
1551
1552 function updatePlayIcon() {
1553 if (!ytPlayer || !playIcon) return;
1554 var state = ytPlayer.getPlayerState();
1555 playIcon.querySelector("path").setAttribute("d", state === 1 ? pausePath : playPath);
1556 }
1557
1558 // --- Progress bar ---
1559 var progressAnimId = null;
1560 var isSeeking = false;
1561 var progressLoopStarted = false;
1562
1563 function startProgressLoop() {
1564 if (progressLoopStarted) return;
1565 progressLoopStarted = true;
1566 progressAnimId = requestAnimationFrame(updateProgress);
1567 }
1568
1569 function updateProgress() {
1570 if (!ytPlayer || isSeeking) { progressAnimId = requestAnimationFrame(updateProgress); return; }
1571 try {
1572 var current = typeof ytPlayer.getCurrentTime === "function" ? ytPlayer.getCurrentTime() : 0;
1573 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
1574 if (duration > 0) {
1575 var pct = (current / duration) * 100;
1576 progressBar.style.width = pct + "%";
1577 progressThumb.style.left = pct + "%";
1578 timeCurrent.textContent = formatTime(current);
1579 timeDuration.textContent = formatTime(duration);
1580 }
1581 if (typeof ytPlayer.getVideoLoadedFraction === "function") {
1582 progressBuffered.style.width = (ytPlayer.getVideoLoadedFraction() * 100) + "%";
1583 }
1584 updatePlayIcon();
1585 } catch (e) {
1586 // Player API not ready yet, retry on next frame
1587 }
1588 progressAnimId = requestAnimationFrame(updateProgress);
1589 }
1590
1591 // Seek on click / drag
1592 if (progressWrap) {
1593 function seekFromEvent(e) {
1594 var rect = progressWrap.getBoundingClientRect();
1595 var x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
1596 var pct = x / rect.width;
1597 var duration = typeof ytPlayer.getDuration === "function" ? ytPlayer.getDuration() : 0;
1598 if (duration > 0 && ytPlayer) {
1599 ytPlayer.seekTo(pct * duration, true);
1600 progressBar.style.width = (pct * 100) + "%";
1601 progressThumb.style.left = (pct * 100) + "%";
1602 timeCurrent.textContent = formatTime(pct * duration);
1603 }
1604 }
1605
1606 progressWrap.addEventListener("mousedown", function(e) {
1607 isSeeking = true;
1608 seekFromEvent(e);
1609 function onMove(e) { seekFromEvent(e); }
1610 function onUp() {
1611 isSeeking = false;
1612 document.removeEventListener("mousemove", onMove);
1613 document.removeEventListener("mouseup", onUp);
1614 }
1615 document.addEventListener("mousemove", onMove);
1616 document.addEventListener("mouseup", onUp);
1617 });
1618
1619 progressWrap.addEventListener("mouseenter", function() {
1620 progressThumb.style.display = "block";
1621 });
1622 progressWrap.addEventListener("mouseleave", function() {
1623 if (!isSeeking) progressThumb.style.display = "none";
1624 });
1625 }
1626
1627 // --- Volume ---
1628 var volHideTimer = null;
1629 var localVol = 100;
1630 var localMuted = false;
1631
1632 function updateVolIconFromLocal() {
1633 if (!volIcon) return;
1634 var path;
1635 if (localMuted || localVol === 0) { path = volIconMuted; }
1636 else if (localVol < 40) { path = volIconLow; }
1637 else if (localVol < 75) { path = volIconMed; }
1638 else { path = volIconHigh; }
1639 volIcon.querySelector("path").setAttribute("d", path);
1640 }
1641
1642 // Called from schedule360AvailabilityChecks — only syncs icon, never touches slider
1643 function updateVolIcon() {
1644 updateVolIconFromLocal();
1645 }
1646
1647 function showVolSlider() {
1648 if (volHideTimer) { clearTimeout(volHideTimer); volHideTimer = null; }
1649 if (volSliderWrap) volSliderWrap.style.display = "flex";
1650 }
1651 function hideVolSlider() {
1652 volHideTimer = setTimeout(function() { if (volSliderWrap) volSliderWrap.style.display = "none"; }, 400);
1653 }
1654
1655 if (muteBtn) {
1656 muteBtn.addEventListener("click", function() {
1657 if (!ytPlayer) return;
1658 if (localMuted) {
1659 ytPlayer.unMute();
1660 localMuted = false;
1661 if (localVol === 0) { localVol = 100; ytPlayer.setVolume(100); }
1662 if (volSlider) volSlider.value = localVol;
1663 } else {
1664 ytPlayer.mute();
1665 localMuted = true;
1666 if (volSlider) volSlider.value = 0;
1667 }
1668 updateVolIconFromLocal();
1669 updateVolSliderFill();
1670 });
1671 muteBtn.addEventListener("mouseenter", showVolSlider);
1672 muteBtn.addEventListener("mouseleave", hideVolSlider);
1673 }
1674 if (volSliderWrap) {
1675 volSliderWrap.addEventListener("mouseenter", showVolSlider);
1676 volSliderWrap.addEventListener("mouseleave", hideVolSlider);
1677 }
1678 function updateVolSliderFill() {
1679 if (!volSlider) return;
1680 var v = parseInt(volSlider.value, 10);
1681 volSlider.style.background = "linear-gradient(to right,#fff " + v + "%,rgba(255,255,255,0.3) " + v + "%)";
1682 }
1683 if (volSlider) {
1684 volSlider.addEventListener("input", function() {
1685 if (!ytPlayer) return;
1686 var val = parseInt(volSlider.value, 10);
1687 localVol = val;
1688 ytPlayer.setVolume(val);
1689 if (val === 0) {
1690 ytPlayer.mute();
1691 localMuted = true;
1692 } else if (localMuted) {
1693 ytPlayer.unMute();
1694 localMuted = false;
1695 }
1696 updateVolIconFromLocal();
1697 updateVolSliderFill();
1698 });
1699 updateVolSliderFill();
1700 }
1701
1702 // --- Fullscreen ---
1703 if (outerContainer) {
1704 var savedFrameWidth = frameEl ? frameEl.style.width : "";
1705 var savedFrameHeight = frameEl ? frameEl.style.height : "";
1706 var savedFrameMaxWidth = frameEl ? frameEl.style.maxWidth : "";
1707 var savedFrameBorderRadius = frameEl ? frameEl.style.borderRadius : "";
1708 var savedContainerBackground = outerContainer.style.background;
1709 if(fsBtn){
1710 fsBtn.addEventListener("click", function() {
1711 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
1712 if (fsEl) {
1713 (document.exitFullscreen || document.webkitExitFullscreen).call(document);
1714 } else {
1715 (outerContainer.requestFullscreen || outerContainer.webkitRequestFullscreen).call(outerContainer);
1716 }
1717 });
1718 }
1719 function onFsChange() {
1720 var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
1721 var isFs = (fsEl === outerContainer);
1722 frameEl.style.width = isFs ? "100%" : savedFrameWidth;
1723 frameEl.style.height = isFs ? "100%" : savedFrameHeight;
1724 frameEl.style.maxWidth = isFs ? "100%" : savedFrameMaxWidth;
1725 frameEl.style.borderRadius = isFs ? "0" : savedFrameBorderRadius;
1726 outerContainer.style.background = isFs ? "#000" : savedContainerBackground;
1727 fsBtn.querySelector("svg path").setAttribute("d", isFs
1728 ? "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"
1729 : "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"
1730 );
1731 schedule360AvailabilityChecks();
1732 }
1733 document.addEventListener("fullscreenchange", onFsChange);
1734 document.addEventListener("webkitfullscreenchange", onFsChange);
1735 }
1736
1737 // Sync controls after player ready
1738 var origSchedule = schedule360AvailabilityChecks;
1739 schedule360AvailabilityChecks = function() {
1740 origSchedule();
1741 updateVolIcon();
1742 updatePlayIcon();
1743 };
1744 });
1745 </script>';
1746 $html .= '</div>'; // Close container
1747 $html .= '</div>'; // Close outer container
1748 return $html;
1749 }
1750 /**
1751 * Prepare shortcode data for Vimeo url
1752 *
1753 * @param array $postdata
1754 * @param mixed $width
1755 * @param mixed $height
1756 *
1757 * @return string
1758 * @since 8.0.0
1759 */
1760 public function prepare_vimeo_video_shortcode_data($postdata, $width, $height, $autoplay, $loop, $radius)
1761 {
1762 $autoplay_param = ($autoplay == 'on') ? 'autoplay=1&muted=1' : '';
1763 $loop_param = ($loop == 'on') ? 'loop=1' : '';
1764
1765 $vidurl_parts = explode('/', rtrim( $postdata['vidurl'], '/' ) );
1766 $vid_id = end($vidurl_parts);
1767
1768 $query_parts = array_filter( [ $autoplay_param, $loop_param ] );
1769 $foundid = !empty($query_parts) ? $vid_id . '?' . implode('&', $query_parts) : $vid_id;
1770
1771 $iframe_id = 'wpvr-vimeo-' . wp_rand( 10000, 99999 );
1772 $do_autoplay = ( $autoplay == 'on' ) ? 'true' : 'false';
1773 $do_loop = ( $loop == 'on' ) ? 'true' : 'false';
1774
1775 $html = '';
1776 $html .= '<div style="text-align: center; max-width:100%; width:' . $width . '; height:' . $height . '; margin: 0 auto;">';
1777 $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>';
1778 $html .= '</div>';
1779 $html .= '<script>
1780 (function() {
1781 var IFRAME_ID = ' . wp_json_encode( $iframe_id ) . ';
1782 var DO_AUTOPLAY = ' . $do_autoplay . ';
1783 var DO_LOOP = ' . $do_loop . ';
1784
1785 function wpvrLoadVimeoSdk( callback ) {
1786 if ( window.Vimeo && window.Vimeo.Player ) { callback(); return; }
1787 window.wpvrVimeoSdkCallbacks = window.wpvrVimeoSdkCallbacks || [];
1788 window.wpvrVimeoSdkCallbacks.push( callback );
1789 if ( window.wpvrVimeoSdkLoading ) { return; }
1790 window.wpvrVimeoSdkLoading = true;
1791 var tag = document.createElement( "script" );
1792 tag.src = "https://player.vimeo.com/api/player.js";
1793 tag.onload = function() {
1794 var cbs = window.wpvrVimeoSdkCallbacks || [];
1795 while ( cbs.length ) { ( cbs.shift() )(); }
1796 };
1797 document.head.appendChild( tag );
1798 }
1799
1800 function initVimeoPlayer() {
1801 var iframeEl = document.getElementById( IFRAME_ID );
1802 if ( !iframeEl ) { return; }
1803 var player = new Vimeo.Player( iframeEl );
1804
1805 player.ready().then( function() {
1806 if ( DO_LOOP ) {
1807 player.setLoop( true ).catch( function() {} );
1808 }
1809 if ( DO_AUTOPLAY ) {
1810 player.setVolume( 0 ).then( function() {
1811 player.play().catch( function() {} );
1812 } ).catch( function() {
1813 player.play().catch( function() {} );
1814 } );
1815 }
1816 } );
1817 }
1818
1819 if ( document.readyState === "loading" ) {
1820 document.addEventListener( "DOMContentLoaded", function() { wpvrLoadVimeoSdk( initVimeoPlayer ); } );
1821 } else {
1822 wpvrLoadVimeoSdk( initVimeoPlayer );
1823 }
1824 }());
1825 </script>';
1826 return $html;
1827 }
1828
1829
1830 /**
1831 * Prepare shortcode data from regular postdata
1832 *
1833 * @param integer $id
1834 * @param array $postdata
1835 * @param string $width
1836 * @param string $height
1837 *
1838 * @return string
1839 * @since 8.0.0
1840 */
1841 public function prepare_regular_video_shortcode_data($id, $postdata, $width, $height, $radius)
1842 {
1843 $html = '';
1844 $html .= '<div id="pano' . $id . '" class="pano-wrap" style="max-width:100%; width: ' . $width . '; height: ' . $height . '; border-radius: ' . $radius . '; margin: 0 auto;">';
1845 $html .= '<div style="width:100%; height:100%; ">' . $postdata['panoviddata'] . '</div>';
1846
1847 $html .= '
1848 <style>
1849 .video-js {
1850 border-radius:' . $radius . ';
1851 }
1852 .video-js canvas{
1853 border-radius:' . $radius . ';
1854 }
1855 #pano' . $id . ' .vjs-poster {
1856 border-radius: ' . $radius . ';
1857 }
1858 </style>
1859
1860 ';
1861
1862 $html .= '</div>';
1863
1864 //video js vr setup //
1865 $html .= '<script>';
1866 $html .= '
1867 (function (window, videojs) {
1868 var player = window.player = videojs("' . $postdata['vidid'] . '");
1869 player.mediainfo = player.mediainfo || {};
1870 player.mediainfo.projection = "equirectangular";
1871
1872 // AUTO is the default and looks at mediainfo
1873 var vr = window.vr = player.vr({ projection: "AUTO", debug: true, forceCardboard: false, antialias: false });
1874 }(window, window.videojs));
1875
1876 ';
1877 $html .= '</script>';
1878 //video js vr end //
1879 return $html;
1880 }
1881
1882
1883 /**
1884 * Prepare scene data for shortcode
1885 *
1886 * @param array $panodata
1887 *
1888 * @return array
1889 * @since 8.0.0
1890 */
1891 public function prepare_shortcode_scene_data($panodata)
1892 {
1893 $scene_data = array();
1894
1895 if (!empty($panodata["scene-list"])) {
1896 $scene_data = $this->prepare_shortcode_scene_list($panodata);
1897 }
1898
1899 return $scene_data;
1900 }
1901
1902
1903 /**
1904 * Prepare scene lists data for shortcode
1905 *
1906 * @param array $panodata
1907 *
1908 * @return array
1909 * @since 8.0.0
1910 */
1911 private function prepare_shortcode_scene_list($panodata)
1912 {
1913 $scene_data = array();
1914 foreach ($panodata["scene-list"] as $panoscenes) {
1915 $hotspot_datas = array();
1916 if (isset($panoscenes["hotspot-list"])) {
1917 $hotspot_datas = $panoscenes["hotspot-list"];
1918 }
1919 $hotspots = $this->prepare_shortcode_hotspots_list($hotspot_datas);
1920 $device_scene = $this->get_shortcode_device_scene($panoscenes['scene-attachment-url']);
1921 $scene_info = $this->get_shortcode_scene_info($panoscenes, $hotspots, $device_scene);
1922 $scene_data[$panoscenes["scene-id"]] = $scene_info;
1923 }
1924 return $scene_data;
1925 }
1926
1927
1928 /**
1929 * Prepare hotspots list for shortcode
1930 *
1931 * @param array $hotspot_datas
1932 *
1933 * @return array
1934 * @since 8.0.0
1935 */
1936 private function prepare_shortcode_hotspots_list($hotspot_datas)
1937 {
1938 $hotspots = array();
1939 foreach ($hotspot_datas as $hotspot_data) {
1940 $hotspot_type = $hotspot_data["hotspot-type"] !== 'scene' ? 'info' : $hotspot_data["hotspot-type"];
1941 $hotspot_content = $hotspot_data["hotspot-content"];
1942 $hotspot_info = $this->get_shortcode_hotspot_info($hotspot_data, $hotspot_type, $hotspot_content);
1943 array_push($hotspots, $hotspot_info);
1944 }
1945 return $hotspots;
1946 }
1947
1948
1949 /**
1950 * Return hotspot info array to hotspot list
1951 *
1952 * @param array $hotspot_data
1953 * @param string $hotspot_type
1954 * @param string $hotspot_content
1955 *
1956 * @return array
1957 * @since 8.0.0
1958 */
1959 private function get_shortcode_hotspot_info($hotspot_data, $hotspot_type, $hotspot_content)
1960 {
1961 $hotspot_info = array(
1962 "text" => $hotspot_data["hotspot-title"],
1963 "pitch" => $hotspot_data["hotspot-pitch"],
1964 "yaw" => $hotspot_data["hotspot-yaw"],
1965 "type" => $hotspot_type,
1966 "cssClass" => $hotspot_data["hotspot-customclass"],
1967 "URL" => $hotspot_data["hotspot-url"],
1968 "wpvr_url_open" => isset($hotspot_data["wpvr_url_open"][0]) ? $hotspot_data["wpvr_url_open"][0] : 'off',
1969 "clickHandlerArgs" => $hotspot_content,
1970 "createTooltipArgs" => $hotspot_data["hotspot-hover"],
1971 "sceneId" => $hotspot_data["hotspot-scene"],
1972 'hotspot_type' => $hotspot_data['hotspot-type']
1973 );
1974
1975 $hotspot_info['URL'] = ($hotspot_data['hotspot-type'] === 'fluent_form' || $hotspot_data['hotspot-type'] === 'wc_product') ? '' : $hotspot_info['URL'];
1976
1977 if ($hotspot_data["hotspot-customclass"] == 'none' || $hotspot_data["hotspot-customclass"] == '') {
1978 unset($hotspot_info["cssClass"]);
1979 }
1980 if (empty($hotspot_data["hotspot-scene"])) {
1981 unset($hotspot_info['targetPitch']);
1982 unset($hotspot_info['targetYaw']);
1983 }
1984 return $hotspot_info;
1985 }
1986
1987
1988 /**
1989 * Return scene info array for scene list
1990 *
1991 * @param array $panoscenes
1992 * @param array $hotspots
1993 * @param string $device_scene
1994 *
1995 * @return array
1996 * @since 8.0.0
1997 */
1998 private function get_shortcode_scene_info($panoscenes, $hotspots, $device_scene)
1999 {
2000 $scene_info = array();
2001 if ($panoscenes["scene-type"] == 'cubemap') {
2002 $pano_attachment = array(
2003 $panoscenes["scene-attachment-url-face0"],
2004 $panoscenes["scene-attachment-url-face1"],
2005 $panoscenes["scene-attachment-url-face2"],
2006 $panoscenes["scene-attachment-url-face3"],
2007 $panoscenes["scene-attachment-url-face4"],
2008 $panoscenes["scene-attachment-url-face5"]
2009 );
2010 $scene_info = array("type" => $panoscenes["scene-type"], "cubeMap" => $pano_attachment, "hotSpots" => $hotspots);
2011 } else {
2012 $scene_info = array("type" => $panoscenes["scene-type"], "panorama" => $device_scene, "hotSpots" => $hotspots);
2013 }
2014 return $scene_info;
2015 }
2016
2017
2018 /**
2019 * Return device scene for shortcode
2020 *
2021 * @param string $device_scene
2022 *
2023 * @return string
2024 * @since 8.0.0
2025 */
2026 private function get_shortcode_device_scene($device_scene)
2027 {
2028 $mobile_media_resize = get_option('mobile_media_resize');
2029 $file_accessible = ini_get('allow_url_fopen');
2030
2031 if ($mobile_media_resize == "true" && $device_scene) {
2032 if ($file_accessible == "1") {
2033 $image_info = getimagesize($device_scene);
2034 if ($image_info[0] > 4096) {
2035 $src_to_id_for_mobile = '';
2036 $src_to_id_for_desktop = '';
2037 if (wpvr_isMobileDevice()) {
2038 $src_to_id_for_mobile = attachment_url_to_postid($device_scene);
2039 if ($src_to_id_for_mobile) {
2040 $mobile_scene = wp_get_attachment_image_src($src_to_id_for_mobile, 'wpvr_mobile');
2041 if ($mobile_scene[3]) {
2042 $device_scene = $mobile_scene[0];
2043 }
2044 }
2045 } else {
2046 $src_to_id_for_desktop = attachment_url_to_postid($device_scene);
2047 if ($src_to_id_for_desktop) {
2048 $desktop_scene = wp_get_attachment_image_src($src_to_id_for_mobile, 'full');
2049 if ($desktop_scene[0]) {
2050 $device_scene = $desktop_scene[0];
2051 }
2052 }
2053 }
2054 }
2055 }
2056 }
2057 return $device_scene;
2058 }
2059
2060
2061 /**
2062 * Prepare json response for shortcode
2063 *
2064 * @param string $autorotation
2065 * @param array $pano_response
2066 * @param string $panoid
2067 * @param integer $autorotationinactivedelay
2068 * @param integer $autorotationstopdelay
2069 *
2070 * @return array
2071 * @since 8.0.0
2072 */
2073 public function prepare_shortcode_response($autorotation, $pano_response, $panoid, $autorotationinactivedelay, $autorotationstopdelay)
2074 {
2075 $pano_id_array = array("panoid" => $panoid);
2076 if (empty($autorotation)) {
2077 unset($pano_response['autoRotate']);
2078 unset($pano_response['autoRotateInactivityDelay']);
2079 unset($pano_response['autoRotateStopDelay']);
2080 }
2081 if (empty($autorotationinactivedelay)) {
2082 unset($pano_response['autoRotateInactivityDelay']);
2083 }
2084 if (empty($autorotationstopdelay)) {
2085 unset($pano_response['autoRotateStopDelay']);
2086 }
2087 $response = array();
2088 $response = array($pano_id_array, $pano_response);
2089 if (!empty($response)) {
2090 $response = json_encode($response);
2091 }
2092 return $response;
2093 }
2094
2095
2096 /**
2097 * Prepare HTML and script contents for scene shortcode
2098 *
2099 * @param mixed $width
2100 * @param mixed $panoid
2101 * @param mixed $hotspoticoncolor
2102 * @param mixed $foreground_color
2103 * @param mixed $hotspotblink
2104 * @param mixed $pulse_color
2105 * @param mixed $radius
2106 * @param mixed $id
2107 * @param mixed $height
2108 * @param mixed $scene_data
2109 * @param mixed $response
2110 * @param mixed $autoload
2111 * @param mixed $default_scene
2112 * @param mixed $postdata
2113 *
2114 * @return string
2115 * @since 8.0.0
2116 */
2117 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)
2118 {
2119 $html = '';
2120 $html .= '<style>';
2121
2122 if ($width == 'embed') {
2123 $html .= 'body{overflow: hidden;}';
2124 }
2125 // Return ID and Classes for CSS styling //
2126 $html = $this->get_shortcode_content_classes($html, $panoid, $hotspoticoncolor, $foreground_color);
2127 // Return webkit keyframes CSS styling //
2128 if ($hotspotblink == 'on') {
2129 $html = $this->get_shortcode_webkit_keyframes($html, $panoid, $pulse_color);
2130 }
2131 $status = get_option('wpvr_edd_license_status');
2132 if ($status !== false && $status == 'valid') {
2133 if (!$gyro) {
2134 $html .= '#' . $panoid . ' div.pnlm-orientation-button {
2135 display: none;
2136 }';
2137 }
2138 } else {
2139 $html .= '#' . $panoid . ' div.pnlm-orientation-button {
2140 display: none;
2141 }';
2142 }
2143 $html .= '</style>';
2144
2145 // Render pano wrapper contents //
2146 $html = $this->get_shortcode_pano_wrap_content($width, $radius, $html, $id, $height, $mobile_height);
2147 // Render hotspots contents //
2148 $html = $this->render_shortcode_hotspot_content($html, $id, $scene_data);
2149 // Render custom iframe content //
2150 $html = $this->render_shortcode_custom_iframe_wrapper($html, $id);
2151
2152 $html .= '</div>';
2153
2154 //script started
2155 $html = $this->get_shortcode_script_content($html, $response, $id, $autoload, $default_scene, $postdata);
2156 //script end
2157 return $html;
2158 }
2159
2160
2161 /**
2162 * Return ID and Classes for CSS styling
2163 *
2164 * @param mixed $html
2165 * @param mixed $panoid
2166 * @param mixed $hotspoticoncolor
2167 * @param mixed $foreground_color
2168 *
2169 * @return string
2170 * @since 8.0.0
2171 */
2172 private function get_shortcode_content_classes($html, $panoid, $hotspoticoncolor, $foreground_color)
2173 {
2174 $html .= '#' . $panoid . ' div.pnlm-hotspot-base.fas,
2175 #' . $panoid . ' div.pnlm-hotspot-base.fab,
2176 #' . $panoid . ' div.pnlm-hotspot-base.fa,
2177 #' . $panoid . ' div.pnlm-hotspot-base.fa-solid,
2178 #' . $panoid . ' div.pnlm-hotspot-base.far {
2179 display: block !important;
2180 background-color: ' . $hotspoticoncolor . ';
2181 color: ' . $foreground_color . ';
2182 border-radius: 100%;
2183 width: 30px;
2184 height: 30px;
2185 animation: icon-pulse' . $panoid . ' 1.5s infinite cubic-bezier(.25, 0, 0, 1);
2186 }';
2187 return $html;
2188 }
2189
2190
2191 /**
2192 * Resturn webkit keyframes CSS styling
2193 *
2194 * @param mixed $html
2195 * @param mixed $panoid
2196 * @param mixed $pulse_color
2197 *
2198 * @return string
2199 * @since 8.0.0
2200 */
2201 private function get_shortcode_webkit_keyframes($html, $panoid, $pulse_color)
2202 {
2203 $html .= '@-webkit-keyframes icon-pulse' . $panoid . ' {
2204 0% {
2205 box-shadow: 0 0 0 0px rgba(' . $pulse_color[0] . ', 1);
2206 }
2207 100% {
2208 box-shadow: 0 0 0 10px rgba(' . $pulse_color[0] . ', 0);
2209 }
2210 }
2211 @keyframes icon-pulse' . $panoid . ' {
2212 0% {
2213 box-shadow: 0 0 0 0px rgba(' . $pulse_color[0] . ', 1);
2214 }
2215 100% {
2216 box-shadow: 0 0 0 10px rgba(' . $pulse_color[0] . ', 0);
2217 }
2218 }';
2219 return $html;
2220 }
2221
2222
2223 /**
2224 * Render pano wrapper content depending on Tour Width, Height and radius
2225 *
2226 * @param mixed $width
2227 * @param mixed $radius
2228 * @param mixed $html
2229 * @param mixed $id
2230 * @param mixed $height
2231 *
2232 * @return string
2233 * @since 8.0.0
2234 */
2235 private function get_shortcode_pano_wrap_content($width, $radius, $html, $id, $height, $mobile_height)
2236 {
2237 if ($width == 'fullwidth') {
2238
2239 if (wpvr_isMobileDevice()) {
2240 if ($radius) {
2241 $html .= '<div id="pano' . $id . '" class="pano-wrap" style="text-align:center; border-radius:' . $radius . '; direction:ltr;">';
2242 } else {
2243 $html .= '<div id="pano' . $id . '" class="pano-wrap" style="text-align:center;">';
2244 }
2245 } else {
2246 if ($radius) {
2247 $html .= '<div id="pano' . $id . '" class="pano-wrap vrfullwidth" style=" text-align:center; height: ' . $height . '; border-radius:' . $radius . '; direction:ltr;" >';
2248 } else {
2249 $html .= '<div id="pano' . $id . '" class="pano-wrap vrfullwidth" style=" text-align:center; height: ' . $height . '; direction:ltr;" >';
2250 }
2251 }
2252 } elseif ($width == 'embed') {
2253 $html .= '<div id="pano' . $id . '" class="pano-wrap vrembed" style=" text-align:center; direction:ltr;" >';
2254 } else {
2255 if (wpvr_isMobileDevice()) {
2256 if ($radius) {
2257 $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;">';
2258 } else {
2259 $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;">';
2260 }
2261 } else {
2262 if ($radius) {
2263 $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;">';
2264 } else {
2265 $html .= '<div id="pano' . $id . '" class="pano-wrap" style=" text-align:center; max-width:100%; width: ' . $width . '; height: ' . $height . '; margin: 0 auto; direction:ltr;">';
2266 }
2267 }
2268 }
2269 return $html;
2270 }
2271
2272
2273 /**
2274 * Render hotspot contents inside pano wrapper
2275 *
2276 * @param string $html
2277 * @param mixed $id
2278 * @param array $scene_data
2279 *
2280 * @return string
2281 * @since 8.0.0
2282 */
2283 private function render_shortcode_hotspot_content($html, $id, $scene_data)
2284 {
2285 $html .= '<div class="wpvr-hotspot-tweak-contents-wrapper" style="display: none">';
2286 $html .= '<i class="fa fa-times cross" data-id="' . $id . '"></i>';
2287 $html .= '<div class="wpvr-hotspot-tweak-contents-flex">';
2288 $html .= '<div class="wpvr-hotspot-tweak-contents">';
2289 ob_start();
2290 do_action('wpvr_hotspot_tweak_contents', $scene_data);
2291 $hotspot_content = ob_get_clean();
2292 $html .= $hotspot_content;
2293 $html .= '</div>';
2294 $html .= '</div>';
2295 $html .= '</div>';
2296 return $html;
2297 }
2298
2299
2300 /**
2301 * Render custom iframe wrapper inside pano wrapper
2302 *
2303 * @param string $html
2304 * @param mixed $id
2305 *
2306 * @return string
2307 * @since 8.0.0
2308 */
2309 private function render_shortcode_custom_iframe_wrapper($html, $id)
2310 {
2311 $html .= '<div class="custom-ifram-wrapper" style="display: none;">';
2312 $html .= '<i class="fa fa-times cross" data-id="' . $id . '"></i>';
2313
2314 $html .= '<div class="custom-ifram-flex">';
2315 $html .= '<div class="custom-ifram">';
2316 $html .= '</div>';
2317 $html .= '</div>';
2318 $html .= '</div>';
2319 return $html;
2320 }
2321
2322
2323 /**
2324 * Return scripts contents for shortcode
2325 *
2326 * @param string $html
2327 * @param mixed $response
2328 * @param mixed $id
2329 * @param mixed $autoload
2330 * @param mixed $default_scene
2331 * @param mixed $postdata
2332 *
2333 * @return string
2334 * @since 8.0.0
2335 */
2336 private function get_shortcode_script_content($html, $response, $id, $autoload, $default_scene, $postdata)
2337 {
2338 $html .= '<script>';
2339 $html .= 'jQuery(document).ready(function() {';
2340 $html .= 'var response = ' . $response . ';';
2341 $html .= 'var scenes = response[1];';
2342 // Hotspot scripts content //
2343 $html = $this->get_shortcode_hotspot_script_content($html);
2344
2345 $html .= 'var panoshow' . $id . ' = pannellum.viewer(response[0]["panoid"], scenes);';
2346
2347 // Panaromic show scripts content //
2348 $html = $this->render_shortcode_panoshow_script_content($html, $id);
2349 // Auto rotation scripts content //
2350 $html = $this->render_shortcode_autoRotate_content($html, $id);
2351
2352 $html .= 'var touchtime = 0;';
2353
2354 // Return autoload scripts content //
2355 if (!$autoload) {
2356 $html = $this->render_shortcode_autoload_content($html, $id);
2357 }
2358
2359 // JQuery scripts for on-click content for elementor tab //
2360 $previeword = "Click to Load Panorama";
2361 if (isset($postdata['previewtext']) && $postdata['previewtext'] != '') {
2362 $previeword = $postdata['previewtext'];
2363 }
2364 $html = $this->render_elementor_tab_title_click_content($html, $id, $default_scene, $previeword);
2365 // JQuery scripts for on-click content for vr tour tab //
2366 $html = $this->render_vr_tour_tab_click_content($html, $id, $default_scene);
2367
2368 if (isset($previeword ) && $previeword != '') {
2369 $html .= 'jQuery("#pano' . $id . '").children(".pnlm-ui").find(".pnlm-load-button p").text("' . $previeword . '")';
2370 }
2371
2372 $html .= '});';
2373 $html .= '</script>';
2374 return $html;
2375 }
2376
2377
2378 /**
2379 * Return scripts content for hotspot data on shortcode
2380 *
2381 * @param mixed $html
2382 *
2383 * @return string
2384 * @since 8.0.0
2385 */
2386 private function get_shortcode_hotspot_script_content($html)
2387 {
2388 $html .= 'if(scenes) {';
2389 $html .= 'var scenedata = scenes.scenes;';
2390 $html .= 'for(var i in scenedata) {';
2391 $html .= 'var scenehotspot = scenedata[i].hotSpots;';
2392 $html .= 'for(var i = 0; i < scenehotspot.length; i++) {';
2393 $html .= 'if(scenehotspot[i]["clickHandlerArgs"] != "") {';
2394 $html .= 'scenehotspot[i]["clickHandlerFunc"] = wpvrhotspot;';
2395 $html .= '}';
2396 if (wpvr_isMobileDevice() && get_option('dis_on_hover') == "true") {
2397 } else {
2398 $html .= 'if(scenehotspot[i]["createTooltipArgs"] != "") {';
2399 $html .= 'scenehotspot[i]["createTooltipFunc"] = wpvrtooltip;';
2400 $html .= '}';
2401 }
2402 $html .= '}';
2403 $html .= '}';
2404 $html .= '}';
2405 return $html;
2406 }
2407
2408
2409 /**
2410 * Render scripts content for panaromic show on shortcode
2411 *
2412 * @param mixed $html
2413 * @param mixed $id
2414 *
2415 * @return string
2416 * @since 8.0.0
2417 */
2418 private function render_shortcode_panoshow_script_content($html, $id)
2419 {
2420 $html .= 'panoshow' . $id . '.on("load", function (){
2421 setTimeout(() => {
2422 window.dispatchEvent(new Event("resize"));
2423 }, 200);
2424 if (jQuery("#pano' . $id . '").children().children(".pnlm-panorama-info:visible").length > 0) {
2425 jQuery("#controls' . $id . '").css("bottom", "55px");
2426 }
2427 else {
2428 jQuery("#controls' . $id . '").css("bottom", "5px");
2429 }
2430 });';
2431 $html .= 'panoshow' . $id . '.on("render", function (){
2432 window.dispatchEvent(new Event("resize"));
2433 });';
2434 return $html;
2435 }
2436
2437
2438 /**
2439 * Return auto rotation scripts content on shortcode
2440 *
2441 * @param mixed $html
2442 * @param mixed $id
2443 *
2444 * @return string
2445 * @since 8.0.0
2446 */
2447 private function render_shortcode_autoRotate_content($html, $id)
2448 {
2449 $html .= 'if (scenes.autoRotate) {
2450 panoshow' . $id . '.on("load", function (){
2451 setTimeout(function(){ panoshow' . $id . '.startAutoRotate(scenes.autoRotate, 0); }, 3000);
2452 });
2453 panoshow' . $id . '.on("scenechange", function (){
2454 setTimeout(function(){ panoshow' . $id . '.startAutoRotate(scenes.autoRotate, 0); }, 3000);
2455 });
2456 }';
2457 return $html;
2458 }
2459
2460
2461 /**
2462 * Return autoload scripts content on shortcode
2463 *
2464 * @param mixed $html
2465 * @param mixed $id
2466 *
2467 * @return string
2468 * @since 8.0.0
2469 */
2470 private function render_shortcode_autoload_content($html, $id)
2471 {
2472 $html .= 'jQuery(document).ready(function(){
2473 jQuery("#controls' . $id . '").hide();
2474 jQuery("#zoom-in-out-controls' . $id . '").hide();
2475 jQuery("#adcontrol' . $id . '").hide();
2476 jQuery("#pano' . $id . '").find(".pnlm-panorama-info").hide();
2477 });';
2478
2479 $html .= 'panoshow' . $id . '.on("load", function (){
2480 jQuery("#controls' . $id . '").show();
2481 jQuery("#zoom-in-out-controls' . $id . '").show();
2482 jQuery("#adcontrol' . $id . '").show();
2483 jQuery("#pano' . $id . '").find(".pnlm-panorama-info").show();
2484 });';
2485 return $html;
2486 }
2487
2488
2489 /**
2490 * JQuery scripts for on-click content for elementor tab
2491 *
2492 * @param mixed $html
2493 * @param mixed $id
2494 * @param mixed $default_scene
2495 *
2496 * @return string
2497 * @since 8.0.0
2498 */
2499 private function render_elementor_tab_title_click_content($html, $id, $default_scene, $previeword)
2500 {
2501 $html .= 'jQuery(".elementor-tab-title").click(function(){
2502 var element_id;
2503 var pano_id;
2504 var element_id = this.id;
2505 element_id = element_id.split("-");
2506 element_id = element_id[3];
2507 jQuery("#elementor-tab-content-"+element_id).children("div").addClass("awwww");
2508 var pano_id = jQuery(".awwww").attr("id");
2509 jQuery("#elementor-tab-content-"+element_id).children("div").removeClass("awwww");
2510 if (pano_id != undefined) {
2511 pano_id = pano_id.split("o");
2512 pano_id = pano_id[1];
2513 if (pano_id == "' . $id . '") {
2514 jQuery("#pano' . $id . '").children(".pnlm-render-container").remove();
2515 jQuery("#pano' . $id . '").children(".pnlm-ui").remove();
2516 panoshow' . $id . ' = pannellum.viewer(response[0]["panoid"], scenes);
2517 jQuery("#pano' . $id . '").children(".pnlm-ui").find(".pnlm-load-button p").text("' . $previeword . '")
2518 setTimeout(function() {
2519 // panoshow' . $id . '.loadScene("' . $default_scene . '");
2520 window.dispatchEvent(new Event("resize"));
2521 if (jQuery("#pano' . $id . '").children().children(".pnlm-panorama-info:visible").length > 0) {
2522 jQuery("#controls' . $id . '").css("bottom", "55px");
2523 }
2524 else {
2525 jQuery("#controls' . $id . '").css("bottom", "5px");
2526 }
2527 }, 200);
2528 }
2529 }
2530 });';
2531 return $html;
2532 }
2533
2534
2535 /**
2536 * JQuery scripts for on-click content for vr tour tab
2537 *
2538 * @param mixed $html
2539 * @param mixed $id
2540 * @param mixed $default_scene
2541 *
2542 * @return string
2543 * @since 8.0.0
2544 */
2545 private function render_vr_tour_tab_click_content($html, $id, $default_scene)
2546 {
2547 $html .= 'jQuery(".geodir-tab-head dd, #vr-tour-tab").click(function(){
2548 jQuery("#pano' . $id . '").children(".pnlm-render-container").remove();
2549 jQuery("#pano' . $id . '").children(".pnlm-ui").remove();
2550 panoshow' . $id . ' = pannellum.viewer(response[0]["panoid"], scenes);
2551 setTimeout(function() {
2552 panoshow' . $id . '.loadScene("' . $default_scene . '");
2553 window.dispatchEvent(new Event("resize"));
2554 if (jQuery("#pano' . $id . '").children().children(".pnlm-panorama-info:visible").length > 0) {
2555 jQuery("#controls' . $id . '").css("bottom", "55px");
2556 }
2557 else {
2558 jQuery("#controls' . $id . '").css("bottom", "5px");
2559 }
2560 }, 200);
2561 });';
2562 return $html;
2563 }
2564 }
2565