PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.2.0
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.2.0
9.2.0 9.1.4 9.1.3 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 All 224 releases
wpvr / legacy / admin / helpers / class-wpvr-format.php

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

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