1/** 2 * JOAN JS v6.0.9 3 * Author: G & D Enterprises, Inc. 4 * FIXED: Day separation now works with translated day names 5 */ 6 7jQuery(function($) { 8 // Cache for faster subsequent loads 9 let showCache = null; 10 let cacheTime = 0; 11 let errorCount = 0; 12 let maxErrors = 3; 13 const CACHE_DURATION = 60000; // 1 minute cache 14 15 function initializeDarkMode() { 16 const settings = window.joan_ajax ? window.joan_ajax.settings : {}; 17 const darkModeSetting = settings.joan_dark_mode || 'auto'; 18 const allowOverride = settings.joan_dark_mode_override === '1'; 19 20 // Get stored user preference if override is allowed 21 let userPreference = null; 22 if (allowOverride) { 23 try { 24 userPreference = localStorage.getItem('joan_dark_mode_preference'); 25 } catch (e) { 26 // localStorage not available 27 } 28 } 29 30 // Determine if dark mode should be active 31 let shouldUseDarkMode = false; 32 33 switch (darkModeSetting) { 34 case 'dark': 35 shouldUseDarkMode = true; 36 break; 37 case 'light': 38 shouldUseDarkMode = false; 39 break; 40 case 'disabled': 41 shouldUseDarkMode = false; 42 break; 43 case 'auto': 44 default: 45 // Check user preference first, then system preference 46 if (userPreference === 'dark') { 47 shouldUseDarkMode = true; 48 } else if (userPreference === 'light') { 49 shouldUseDarkMode = false; 50 } else { 51 // Fall back to system preference 52 shouldUseDarkMode = window.matchMedia && 53 window.matchMedia('(prefers-color-scheme: dark)').matches; 54 } 55 break; 56 } 57 58 // Apply dark mode class 59 applyDarkMode(shouldUseDarkMode); 60 61 // Add toggle buttons if override is allowed 62 if (allowOverride && darkModeSetting !== 'disabled') { 63 addDarkModeToggles(shouldUseDarkMode); 64 } 65 66 // Listen for system preference changes if in auto mode 67 if (darkModeSetting === 'auto' && window.matchMedia && !userPreference) { 68 const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 69 const handleChange = (e) => { 70 if (!localStorage.getItem('joan_dark_mode_preference')) { 71 applyDarkMode(e.matches); 72 updateToggleButtons(e.matches); 73 } 74 }; 75 76 // Use modern addEventListener if available, fallback to addListener 77 if (mediaQuery.addEventListener) { 78 mediaQuery.addEventListener('change', handleChange); 79 } else if (mediaQuery.addListener) { 80 mediaQuery.addListener(handleChange); 81 } 82 } 83 } 84 85 function applyDarkMode(isDark) { 86 const body = document.body; 87 // Define selectors for all JOAN elements that support theme switching 88 const selectors = [ 89 '.joan-now-playing', 90 '.joan-schedule-container', 91 'table.joan-schedule', 92 '.joan-schedule-day-section', 93 '.joan-schedule-day-table', 94 '.joan-schedule-day-header', 95 '.joan-schedule-controls', 96 '.joan-schedule-empty-day', 97 '.joan-sunday-header', 98 '.joan-monday-header', 99 '.joan-tuesday-header', 100 '.joan-wednesday-header', 101 '.joan-thursday-header', 102 '.joan-friday-header', 103 '.joan-saturday-header', 104 '.joan-sunday-table', 105 '.joan-monday-table', 106 '.joan-tuesday-table', 107 '.joan-wednesday-table', 108 '.joan-thursday-table', 109 '.joan-friday-table', 110 '.joan-saturday-table', 111 '.joan-today-highlight',
112 '.joan-current-show-highlight' 113 ]; 114 // Toggle body classes based on dark/light state 115 if (isDark) { 116 body.classList.add('joan-dark-mode'); 117 body.classList.remove('joan-light-mode'); 118 } else { 119 body.classList.add('joan-light-mode'); 120 body.classList.remove('joan-dark-mode'); 121 } 122 // Toggle corresponding classes on all JOAN elements 123 selectors.forEach(selector => { 124 const elements = document.querySelectorAll(selector); 125 elements.forEach(element => { 126 if (isDark) { 127 element.classList.add('joan-dark-mode'); 128 element.classList.remove('joan-light-mode'); 129 } else { 130 element.classList.add('joan-light-mode'); 131 element.classList.remove('joan-dark-mode'); 132 } 133 }); 134 }); 135 } 136 137 function addDarkModeToggles(currentState) { 138 $('.joan-now-playing').each(function() { 139 const $container = $(this); 140 141 // Don't add toggle if one already exists 142 if ($container.find('.joan-dark-mode-toggle').length > 0) { 143 return; 144 } 145 146 const $toggle = $('<button type="button" class="joan-dark-mode-toggle"></button>'); 147 updateToggleButton($toggle, currentState); 148 149 $toggle.on('click', function(e) { 150 e.preventDefault(); 151 toggleDarkMode(); 152 }); 153 154 $container.append($toggle); 155 }); 156 } 157 158 function updateToggleButton($button, isDark) { 159 // TRANSLATION: Use localized strings for dark mode toggle 160 const i18n = joan_ajax.i18n || {}; 161 const lightModeText = i18n.switch_to_light_mode || 'Switch to light mode'; 162 const darkModeText = i18n.switch_to_dark_mode || 'Switch to dark mode'; 163 164 $button.text(isDark ? 'ð' : 'âï¸') 165 .attr('title', isDark ? lightModeText : darkModeText) 166 .attr('aria-label', isDark ? lightModeText : darkModeText); 167 } 168 169 function updateToggleButtons(isDark) { 170 $('.joan-dark-mode-toggle').each(function() { 171 updateToggleButton($(this), isDark); 172 }); 173 } 174 175 function toggleDarkMode() { 176 const isDarkMode = document.body.classList.contains('joan-dark-mode'); 177 const newState = !isDarkMode; 178 179 // Apply the change 180 applyDarkMode(newState); 181 updateToggleButtons(newState); 182 183 // Store user preference 184 try { 185 localStorage.setItem('joan_dark_mode_preference', newState ? 'dark' : 'light'); 186 } catch (e) { 187 // localStorage not available 188 } 189 } 190 191 function getPreferredTimezone() { 192 return localStorage.getItem("joan_timezone") || Intl.DateTimeFormat().resolvedOptions().timeZone; 193 } 194 195 function setPreferredTimezone(tz) { 196 localStorage.setItem("joan_timezone", tz); 197 } 198 199 /** 200 * Escape HTML special characters in a string. 201 * 202 * JavaScript templates in this script interpolate strings directly into 203 * HTML. To avoid introducing markup or XSS vulnerabilities when 204 * displaying show names or host names, we replace special characters 205 * with their HTML entities. Similar to PHP's esc_html(), this 206 * function ensures that dynamic content is rendered safely. 207 * 208 * @param {string} str The raw string to escape. 209 * @returns {string} The escaped string. 210 */ 211 function escapeHtml(str) { 212 return String(str) 213 .replace(/&/g, '&') 214 .replace(/</g, '<') 215 .replace(/>/g, '>') 216 .replace(/"/g, '"') 217 .replace(/'/g, '''); 218 } 219 220 function buildTimezoneDropdown(currentTZ, container, settings) { 221 // Check if timezone selector should be shown based on settings 222 const allowTimezoneSelector = settings.joan_allow_timezone_selector === '1'; 223 const showTimezone = container.data('show-timezone') !== 'no' && allowTimezoneSelector; 224 225 if (!showTimezone || container.find('.joan-timezone-selector').length) return; 226 227 const wrapper = document.createElement('div'); 228 wrapper.className = 'joan-timezone-selector'; 229 230 const label = document.createElement('label'); 231 // TRANSLATION: Use localized string 232 label.textContent = joan_ajax.i18n.switch_timezone; 233 label.htmlFor = "joan-tz-select-" + Math.random().toString(36).substr(2, 9); 234 235 const select = document.createElement('select'); 236 select.id = label.htmlFor; 237 238 const timezones = Intl.supportedValuesOf('timeZone');
239 timezones.forEach(zone => { 240 const opt = document.createElement('option'); 241 opt.value = zone; 242 opt.textContent = zone; 243 if (zone === currentTZ) opt.selected = true; 244 select.appendChild(opt); 245 }); 246 247 // Add time display within timezone selector 248 const timeDisplay = document.createElement('div'); 249 timeDisplay.className = 'joan-timezone-time'; 250 251 // Function to update the time display 252 function updateTimezoneTime() { 253 const now = new Date(); 254 const selectedTimezone = select.value || currentTZ; 255 const timeFormat = settings.time_format || '12'; 256 257 let formatted; 258 try { 259 if (timeFormat === '24') { 260 formatted = now.toLocaleTimeString(undefined, { 261 timeZone: selectedTimezone, 262 hour: '2-digit', 263 minute: '2-digit', 264 hour12: false, 265 timeZoneName: 'short' 266 }); 267 } else { 268 formatted = now.toLocaleTimeString(undefined, { 269 timeZone: selectedTimezone, 270 hour: 'numeric', 271 minute: '2-digit', 272 hour12: true, 273 timeZoneName: 'short' 274 }); 275 } 276 // TRANSLATION: Use localized string 277 timeDisplay.textContent = joan_ajax.i18n.current_time + " " + formatted; 278 } catch (e) { 279 // TRANSLATION: Use localized string 280 timeDisplay.textContent = joan_ajax.i18n.time_display_unavailable; 281 } 282 } 283 284 select.addEventListener('change', function () { 285 setPreferredTimezone(this.value); 286 updateTimezoneTime(); // Update time display immediately 287 clearScheduleCache(); // FIX: Clear cache on timezone change 288 errorCount = 0; // Reset error count on user interaction 289 fetchCurrentShow(); // re-render on change 290 }); 291 292 wrapper.appendChild(label); 293 wrapper.appendChild(select); 294 wrapper.appendChild(timeDisplay); 295 296 // Insert timezone selector appropriately 297 const existingTimeDisplay = container.find('.joan-current-time'); 298 if (existingTimeDisplay.length > 0) { 299 // Replace the basic time display with enhanced timezone selector 300 existingTimeDisplay.replaceWith(wrapper); 301 } else { 302 // Add at the beginning 303 container[0].insertBefore(wrapper, container[0].firstChild); 304 } 305 306 // Initial time update 307 updateTimezoneTime(); 308 309 // Update time every minute 310 setInterval(updateTimezoneTime, 60000); 311 } 312 313 // FIX: Add cache clearing function 314 function clearScheduleCache() { 315 showCache = null; 316 cacheTime = 0; 317 console.log('JOAN: Schedule cache cleared'); 318 } 319 320 // FIX: Check for admin updates by monitoring page focus 321 function initializeCacheInvalidation() { 322 // Clear cache when page regains focus (user might have made admin changes) 323 let lastFocusTime = Date.now(); 324 325 window.addEventListener('focus', function() { 326 const now = Date.now(); 327 // If page was out of focus for more than 30 seconds, clear cache 328 if (now - lastFocusTime > 30000) { 329 clearScheduleCache(); 330 console.log('JOAN: Cache cleared due to page refocus after admin activity'); 331 } 332 }); 333 334 window.addEventListener('blur', function() { 335 lastFocusTime = Date.now(); 336 }); 337 338 // Also periodically clear cache to ensure fresh data 339 setInterval(function() { 340 if (showCache && (Date.now() - cacheTime) > 300000) { // 5 minutes 341 clearScheduleCache(); 342 } 343 }, 60000); // Check every minute 344 } 345 346 function getImageConstraints(container, settings) { 347 // Get max width from settings or container data 348 let maxWidth = parseInt(settings.widget_max_width) || parseInt(container.data('max-width')) || 300; 349 const minWidth = 150; 350 351 // Ensure constraints are within bounds 352 if (maxWidth < minWidth) maxWidth = minWidth; 353 if (maxWidth > 500) maxWidth = 500; 354 355 // Get container width for responsive sizing 356 const containerWidth = container.width(); 357 358 // Adjust max width based on container size for better responsive behavior 359 if (containerWidth > 0 && containerWidth < maxWidth) { 360 maxWidth = Math.max(containerWidth - 40, minWidth); // Leave some padding 361 } 362 363 return { minWidth, maxWidth, containerWidth }; 364 } 365 366 function formatTime(isoString, settings, timezone = null) { 367 if (!isoString) return ''; 368 369 const date = new Date(isoString); 370 const timeFormat = settings.time_format || '12'; 371 const selectedTimezone = timezone || getPreferredTimezone(); 372 373 if (timeFormat === '24') { 374 return date.toLocaleTimeString([], { 375 timeZone: selectedTimezone, 376 hour: '2-digit', 377 minute: '2-digit', 378 hour12: false 379 }); 380 } else { 381 return date.toLocaleTimeString([], { 382 timeZone: selectedTimezone, 383 hour: 'numeric', 384 minute: '2-digit', 385 hour12: true 386 }); 387 } 388 } 389 390 function formatTimeRange(startIso, endIso, settings, timezone = null) { 391 const startText = formatTime(startIso, settings, timezone); 392 const endText = formatTime(endIso, settings, timezone); 393 return startText && endText ? `${startText} - ${endText}` : ''; 394 } 395 396 function handleImageSizing(imageUrl, showName, container, settings, c
396allback) { 397 // Check if images should be shown 398 if (settings.show_jock_image !== '1' || !imageUrl) { 399 callback('', ''); 400 return; 401 } 402 403 const constraints = getImageConstraints(container, settings); 404 const img = new Image(); 405 406 img.onload = function() { 407 const naturalWidth = this.naturalWidth; 408 const naturalHeight = this.naturalHeight; 409 let imageHtml = ''; 410 let imagePosition = ''; 411 412 // Calculate optimal display size 413 let displayWidth = naturalWidth; 414 let displayHeight = naturalHeight; 415 416 // Constrain to our limits 417 if (displayWidth > constraints.maxWidth) { 418 displayHeight = (displayHeight * constraints.maxWidth) / displayWidth; 419 displayWidth = constraints.maxWidth; 420 } 421 if (displayWidth < constraints.minWidth) { 422 displayHeight = (displayHeight * constraints.minWidth) / displayWidth; 423 displayWidth = constraints.minWidth; 424 } 425 426 // Only use large image placement for very large images
427 if (naturalWidth >= 350 && constraints.containerWidth > 400) { 428 // Very large image: place under time, above "up next" 429 imageHtml = `<div class="joan-image-large"> 430 <img class="joan-thumb joan-thumb-large" 431 src="${imageUrl}" 432 alt="${showName}" /> 433 </div>`; 434 imagePosition = 'bottom'; 435 } else { 436 // All other images: standard placement under show info 437 imageHtml = `<div class="joan-image-standard"> 438 <img class="joan-thumb" 439 src="${imageUrl}" 440 alt="${showName}" /> 441 </div>`; 442 imagePosition = 'standard'; 443 } 444 445 callback(imageHtml, imagePosition); 446 }; 447 448 img.onerror = function() { 449 // If image fails to load, use standard placement 450 const imageHtml = `<div class="joan-image-standard"> 451 <img class="joan-thumb" 452 src="${imageUrl}" 453 alt="${showName}" /> 454 </div>`; 455 callback(imageHtml, 'standard'); 456 }; 457 458 img.src = imageUrl; 459 } 460 461 function applyStatusStyling($container, response) { 462 // Remove any existing status classes 463 $container.removeClass('joan-status-active joan-status-suspended joan-status-off_air'); 464 465 // Get schedule status from response 466 const status = response.schedule_status || 'active'; 467 const statusType = response.status_type || 'active'; 468 469 // Add appropriate status class 470 if (status !== 'active') { 471 $container.addClass('joan-status-' + status); 472 473 // Add body class for global styling 474 $('body').addClass('joan-schedule-' + status); 475 } else { 476 // Remove body classes when active 477 $('body').removeClass('joan-schedule-suspended joan-schedule-off_air'); 478 } 479 480 return { status, statusType }; 481 } 482
483 function renderStatusMessage(response, $container, settings) { 484 const { status } = applyStatusStyling($container, response); 485 486 // Get the appropriate message (with fallback to localized default) 487 let message = response.off_air_message || joan_ajax.i18n.off_air_default; 488 489 // Add status icon based on status type 490 let statusIcon = ''; 491 if (status === 'suspended') { 492 statusIcon = '<span class="joan-status-icon">â¸</span>'; 493 } else if (status === 'off_air') { 494 statusIcon = '<span class="joan-status-icon">ð´</span>'; 495 } 496 497 // Show upcoming show if enabled and available 498 if (settings.show_next_show === '1' && response.upcoming_show && settings.joan_jock_only_mode !== '1') { 499 const upcoming = response.upcoming_show; 500 const timeRange = formatTimeRange(upcoming.start_iso, upcoming.end_iso, settings, getPreferredTimezone()); 501 // TRANSLATION: Use localized string 502 message += `<div class="upcoming-show"> 503 <strong>${joan_ajax.i18n.up_next}</strong> ${upcoming.show_name} 504 ${timeRange ? `<span class="upcoming-time">${timeRange}</span>` : ''} 505 </div>`; 506 } 507 508 $container.html(`<div class="joan-status-message">${statusIcon}${message}</div>`); 509 } 510
511 function renderShow(response, $container) { 512 // Apply max width setting to container 513 const settings = response.settings || joan_ajax.settings || {}; 514 const maxWidth = parseInt(settings.widget_max_width) || 300; 515 $container.css('max-width', maxWidth + 'px'); 516 517 // Apply center title setting 518 const centerTitle = settings.joan_center_widget_title === '1'; 519 if (centerTitle) { 520 $container.addClass('joan-center-title'); 521 } else { 522 $container.removeClass('joan-center-title'); 523 } 524 525 // Check for jock-only mode 526 const jockOnlyMode = settings.joan_jock_only_mode === '1'; 527 if (jockOnlyMode) { 528 $container.addClass('joan-jock-only-mode'); 529 } else { 530 $container.removeClass('joan-jock-only-mode'); 531 } 532 533 // Apply dark mode if appropriate 534 const isDarkMode = document.body.classList.contains('joan-dark-mode'); 535 if (isDarkMode) { 536 $container.addClass('joan-dark-mode'); 537 } else { 538 $container.removeClass('joan-dark-mode'); 539 } 540 541 // Check for off-air conditions or non-active status 542 if (response.schedule_status !== 'active' || !response.current_show) { 543 renderStatusMessage(response, $container, settings); 544 545 // Show timezone selector based on settings, but not in jock-only mode or off-air status 546 const allowTimezoneSelector = settings.joan_allow_timezone_selector === '1'; 547 if (!jockOnlyMode && response.schedule_status !== 'off_air' && allowTimezoneSelector) { 548 buildTimezoneDropdown(getPreferredTimezone(), $container, settings); 549 } 550 551 // Add dark mode toggle if appropriate 552 const allowOverride = settings.joan_dark_mode_override === '1'; 553 const darkModeSetting = settings.joan_dark_mode || 'auto'; 554 if (allowOverride && darkModeSetting !== 'disabled') { 555 addDarkModeToggles(isDarkMode); 556 } 557 558 return; 559 } 560 561 // Apply active status styling 562 applyStatusStyling($container, response); 563 564 const show = response.current_show; 565 const upcoming = response.upcoming_show; 566 567 const selectedTimezone = getPreferredTimezone(); 568 const timeRange = formatTimeRange(show.start_iso, show.end_iso, settings, selectedTimezone); 569 const jock = show.dj_name || ""; 570 const image = show.image_url || ""; 571 const link = show.link_url || ""; 572 573 // Handle jock-only mode rendering 574 if (jockOnlyMode) { 575 let html = ''; 576 577 // In jock-only mode, only show image and time 578 if (image && settings.show_jock_image === '1') { 579 handleImageSizing(image, show.show_name, $container, settings, function(imageHtml, imagePosition) { 580 let jockOnlyHtml = `<div class="joan-show-info"> 581 ${timeRange ? `<div class="show-time">${timeRange}</div>` : ""} 582 </div>`; 583 584 // If a link URL is provided, wrap the image in an anchor so that clicking 585 // the image navigates to the same destination as the host/show link. 586 if (link) { 587 // Replace the <img> tag with a linked version. This regex captures the 588 // entire img tag (including attributes) and wraps it in an <a> element. 589 imageHtml = imageHtml.replace(/(<img[^>]*>)/i, `<a href="${link}" target="_blank">$1</a>`); 590 } 591 592 // Add image after time (simplified for jock-only mode) 593 jockOnlyHtml += imageHtml; 594 595 $container.html(jockOnlyHtml); 596 597 // Add timezone selector if enabled (this will include time in the selector area) 598 const allowTimezoneSelector = settings.joan_allow_timezone_selector === '1'; 599 if (allowTimezoneSelector) { 600 buildTimezoneDropdown(getPreferredTimezone(), $container, settings); 601 } 602 603 // Add dark mode toggle 604 const allowOverride = settings.joan_dark_mode_override === '1'; 605 const darkModeSetting = settings.joan_dark_mode || 'auto'; 606 if (allowOverride && darkModeSetting !== 'disabled') { 607 addDarkModeToggles(isDarkMode); 608 } 609 }); 610 } else { 611 // No image, just time 612 html = `<div class="joan-show-info"> 613 ${timeRange ? `<div class="show-time">${timeRange}</div>` : ""} 614 </div>`; 615 616 $container.html(html); 617 618 // Add timezone selector if enabled 619 const allowTimezoneSelector = settings.joan_allow_timezone_selector === '1'; 620 if (allowTimezoneSelector) { 621 buildTimezoneDropdown(getPreferredTimezone(), $container, settings); 622 } 623 624 // Add dark mode toggle 625 const allowOverride = settings.joan_dark_mode_override === '1'; 626 const darkModeSetting = settings.joan_dark_mode || 'auto'; 627 if (allowOverride && darkModeSetting !== 'disabled') { 628 addDarkModeToggles(isDarkMode); 629 } 630 } 631 632 return; 633 } 634 635 // Regular mode rendering with FIXED image positioning 636 handleImageSizing(image, show.show_name, $container, settings, function(imageHtml, imagePosition) { 637 let html = ''; 638 639 // FIXED: All images now go under show info, no right positioning 640 // Build show and host/jock display using new field usage and link assignment logic. 641 const linkAssignment = settings.joan_link_assignment || 'jock_name'; 642 const jockFieldLabel = settings.joan_jock_field_label || ''; 643 let displayJock = ''; 644 if (jockFieldLabel && jockFieldLabel.trim() !== '') { 645 // If a global host name is provided, it overrides the per-show jock name 646 displayJock = jockFieldLabel.trim(); 647 } else if (jock && jock.trim() !== '') { 648 // Otherwise use the jock name from the show record 649 displayJock = jock.trim(); 650 } 651 652 // Escape the show title for safe HTML output 653 const showTitleEscaped = escapeHtml(show.show_name || ''); 654 let showTitleHtml = showTitleEscaped; 655 // Apply link to show title if link assignment is set to show_title or both 656 if (link && (linkAssignment === 'show_title' || linkAssignment === 'both')) { 657 showTitleHtml = `<a href="${link}" target="_blank">${showTitleEscaped}</a>`; 658 } 659 660 // Build jock/host HTML if a display name exists 661 let jockHtml = ''; 662 if (displayJock) { 663 const jockEscaped = escapeHtml(displayJock); 664 if (link && (linkAssignment === 'jock_name' || linkAssignment === 'both')) { 665 jockHtml = `<a href="${link}" target="_blank">${jockEscaped}</a>`; 666 } else { 667 jockHtml = jockEscaped; 668 } 669 } 670 671 html = `<div class="joan-show-info">
672 <h3 class="current-show">${showTitleHtml}</h3> 673 ${jockHtml ? `<div class="joan-jock">${jockHtml}</div>` : ""} 674 ${timeRange ? `<div class="show-time">${timeRange}</div>` : ""} 675 </div>`; 676 677 // Append the image after show info (standard placement) 678 html += imageHtml; 679 680 // Add upcoming show section if enabled 681 if (settings.show_next_show === '1' && upcoming) { 682 const upTimeRange = formatTimeRange(upcoming.start_iso, upcoming.end_iso, settings, selectedTimezone); 683 // TRANSLATION: Use localized string 684 html += ` 685 <div class="upcoming-show"> 686 <strong>${joan_ajax.i18n.up_next}</strong> ${upcoming.show_name} 687 ${upTimeRange ? `<span class="upcoming-time">${upTimeRange}</span>` : ''} 688 </div> 689 `; 690 } 691 692 $container.html(html); 693 694 // Add timezone selector if enabled (this provides timezone selection without duplicate time) 695 const allowTimezoneSelector = settings.joan_allow_timezone_selector === '1'; 696 if (allowTimezoneSelector) { 697 buildTimezoneDropdown(getPreferredTimezone(), $container, settings); 698 } 699 700 // Add dark mode toggle if appropriate 701 const allowOverride = settings.joan_dark_mode_override === '1'; 702 const darkModeSetting = settings.joan_dark_mode || 'auto'; 703 if (allowOverride && darkModeSetting !== 'disabled') { 704 addDarkModeToggles(isDarkMode); 705 } 706 }); 707 708 // Trigger custom event to update local time displays 709 const event = new CustomEvent('joanSettingsUpdated'); 710 document.dispatchEvent(event); 711 } 712 713 function fetchCurrentShow(force = false) { 714 const now = Date.now(); 715 716 // Stop retrying if too many errors occurred 717 if (errorCount >= maxErrors && !force) { 718 console.warn('JOAN: Stopped retrying due to repeated failures. Refresh page to retry.'); 719 return; 720 } 721 722 // Use cache if available and not expired (for instant loading) 723 if (!force && showCache && (now - cacheTime) < CACHE_DURATION && errorCount === 0) { 724 $('.joan-now-playing').each(function() { 725 renderShow(showCache, $(this)); 726 }); 727 return; 728 } 729 730 $('.joan-now-playing').each(function() { 731 const $container = $(this); 732 733 // Show loading only for slow connections (after 1 second for first try, 3 seconds for retries) 734 const loadingDelay = errorCount > 0 ? 3000 : 1000; 735 const loadingTimer = setTimeout(() => { 736 if ($container.html().indexOf('Loading') === -1 && $container.html().indexOf('Unable') === -1) { 737 // TRANSLATION: Use localized strings 738 const loadingText = errorCount > 0 ? joan_ajax.i18n.retrying : joan_ajax.i18n.loading; 739 $container.html(`<div class="joan-loading">${loadingText}</div>`); 740 } 741 }, loadingDelay); 742 743 // Rocket Loader compatibility - delay AJAX request 744 const ajaxDelay = window.rocketLoaderDelay || (typeof rocket_loader !== 'undefined' ? 1000 : 0); 745 746 setTimeout(() => { 747 // Check if joan_ajax is properly loaded 748 if (typeof joan_ajax === 'undefined' || !joan_ajax.ajaxurl) { 749 console.error('JOAN: joan_ajax not loaded properly'); 750 // TRANSLATION: Use localized strings 751 $container.html('<div class="joan-error">' + joan_ajax.i18n.config_error + ' <a href="#" onclick="window.location.reload(); return false;">' + joan_ajax.i18n.refresh_page + '</a></div>'); 752 return; 753 } 754 755 console.log('JOAN: Sending AJAX request to:', joan_ajax.ajaxurl); 756 757 $.ajax({ 758 url: joan_ajax.ajaxurl, 759 type: 'POST', 760 data: { 761 action: 'show_time_curd', 762 'crud-action': 'read', 763 'read-type': 'current' 764 }, 765 timeout: 30000, 766 cache: false, 767 dataType: 'json', 768 beforeSend: function(xhr, ajaxSettings) { 769 console.log('JOAN: AJAX request starting...', ajaxSettings.data); 770 xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); 771 xhr.setRequestHeader('Pragma', 'no-cache'); 772 xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 773 } 774 }) 775 .done(function(response) { 776 console.log('JOAN: AJAX success:', response); 777 clearTimeout(loadingTimer); 778 779 errorCount = 0; 780 showCache = response;
781 cacheTime = now; 782 783 renderShow(response, $container); 784 }) 785 .fail(function(xhr, status, error) { 786 console.error('JOAN: AJAX failed:', { 787 status: status, 788 error: error, 789 responseText: xhr.responseText, 790 readyState: xhr.readyState, 791 statusCode: xhr.status 792 }); 793 794 clearTimeout(loadingTimer); 795 errorCount++; 796 797 // TRANSLATION: Use localized strings for all error messages 798 let errorMessage = joan_ajax.i18n.unable_to_load + ' '; 799 if (status === 'timeout') { 800 if (errorCount === 1) { 801 errorMessage = joan_ajax.i18n.server_slow + ' '; 802 } else { 803 errorMessage = joan_ajax.i18n.connection_timeout + ` (attempt ${errorCount}/${maxErrors}). `; 804 } 805 } else if (status === 'error') { 806 if (xhr.status === 0) { 807 errorMessage = joan_ajax.i18n.request_blocked + ' '; 808 } else if (xhr.status === 403) { 809 errorMessage = joan_ajax.i18n.access_denied + ' '; 810 } else if (xhr.status === 500) { 811 errorMessage = joan_ajax.i18n.server_internal_error + ' '; 812 } else { 813 errorMessage = joan_ajax.i18n.server_error + ` (${xhr.status}). `; 814 } 815 } else if (status === 'parsererror') { 816 errorMessage = joan_ajax.i18n.invalid_response + ' '; 817 } 818 819 if (errorCount >= maxErrors) { 820 // TRANSLATION: Use localized string for automatic retries stopped 821 errorMessage += '<strong>' + joan_ajax.i18n.automatic_retries_stopped + '</strong> '; 822 } 823 824 errorMessage += '<a href="#" onclick="window.location.reload(); return false;">' + joan_ajax.i18n.refresh_page + '</a>'; 825 826 $container.html(`<div class="joan-error">${errorMessage}</div>`); 827 }); 828 }, ajaxDelay); 829 }); 830 } 831 832 // ============================================================================ 833 // FIXED: separateScheduleByDays() - Now works with translated day names 834 // ============================================================================ 835 function separateScheduleByDays() { 836 $('table.joan-schedule').each(function() { 837 const $table = $(this); 838 839 if ($table.hasClass('joan-schedule-processed')) { 840 return; 841 } 842 843 // Check if this is a filtered schedule (should not be separated by days) 844 const isFilteredSchedule = 845 $table.attr('data-filter-day') !== undefined || 846 $table.data('filter-day') !== undefined || 847 $table.hasClass('joan-filtered-schedule') || 848 $table.closest('.joan-filtered-schedule').length > 0 || 849 $table.closest('[data-filter-day]').length > 0 || 850 $table.find('tbody tr').length <= 5 && 851 (function() { 852 const days = new Set(); 853 $table.find('tbody tr').each(function() { 854 const dayText = $(this).find('td').eq(1).text().trim(); 855 if (dayText) days.add(dayText); 856 }); 857 return days.size <= 1; 858 })(); 859 860 if (isFilteredSchedule) { 861 console.log('JOAN: Skipping day separation for filtered schedule table'); 862 $table.addClass('joan-schedule-processed'); 863 return; 864 } 865 866 $table.addClass('joan-schedule-processed'); 867 868 const $rows = $table.find('tbody tr'); 869 if ($rows.length === 0) { 870 return; 871 } 872 873 // CRITICAL FIX: Get translated day names from localized script 874 const translatedDays = joan_ajax.i18n && joan_ajax.i18n.days ? joan_ajax.i18n.days : { 875 'Sunday': 'Sunday', 876 'Monday': 'Monday', 877 'Tuesday': 'Tuesday', 878 'Wednesday': 'Wednesday', 879 'Thursday': 'Thursday', 880 'Friday': 'Friday', 881 'Saturday': 'Saturday' 882 }; 883 884 // Build dayGroups using English keys 885 const englishDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 886 const dayGroups = {}; 887 888 englishDays.forEach(day => { 889 dayGroups[day] = []; 890 }); 891 892 // Create reverse mapping: translated name -> English name 893 const translatedToEnglish = {};
894 Object.keys(translatedDays).forEach(englishDay => { 895 const translatedDay = translatedDays[englishDay]; 896 translatedToEnglish[translatedDay] = englishDay; 897 }); 898 899 const $headerRow = $table.find('thead tr').clone(); 900 901 // FIXED: Process rows and group by English day name 902 $rows.each(function() { 903 const $row = $(this); 904 905 // Try to get day from data attribute first (always English) 906 let englishDay = $row.attr('data-day') || $row.data('day'); 907 908 if (!englishDay) { 909 // Fallback: get from table cell and translate back to English 910 const dayCell = $row.find('td').eq(1); 911 const displayDay = dayCell.text().trim(); 912 englishDay = translatedToEnglish[displayDay] || displayDay; 913 } 914 915 if (dayGroups.hasOwnProperty(englishDay)) { 916 dayGroups[englishDay].push($row); 917 } 918 }); 919 920 // Check if we need day separation 921 const daysWithShows = englishDays.filter(day => dayGroups[day].length > 0); 922 if (daysWithShows.length <= 1) { 923 console.log('JOAN: Skipping day separation - only one day has shows'); 924 return; 925 } 926
927 const $container = $('<div class="joan-schedule-container"></div>'); 928 const uniqueId = 'joan-day-filter-' + Math.random().toString(36).substr(2, 9); 929 930 const i18n = joan_ajax.i18n || {}; 931 932 // Build day filter dropdown with translated names 933 const $controls = $(` 934 <div class="joan-schedule-controls"> 935 <label for="${uniqueId}">${i18n.filter_by_day || 'Filter by day:'}</label> 936 <select id="${uniqueId}" class="joan-day-filter"> 937 <option value="">${i18n.all_days || 'All Days'}</option> 938 </select> 939 </div> 940 `); 941 942 const $select = $controls.find('select'); 943 944 // Add options using translated day names 945 englishDays.forEach(englishDay => { 946 if (dayGroups[englishDay].length > 0) { 947 const translatedName = translatedDays[englishDay] || englishDay; 948 $select.append(`<option value="${englishDay}">${translatedName}</option>`); 949 } 950 }); 951 952 $container.append($controls); 953 954 const dayClassMap = { 955 'Sunday': 'sunday', 956 'Monday': 'monday', 957 'Tuesday': 'tuesday', 958 'Wednesday': 'wednesday', 959 'Thursday': 'thursday', 960 'Friday': 'friday', 961 'Saturday': 'saturday' 962 }; 963 964 const dayEmojis = { 965 'Sunday': 'He', 966 'Monday': 'Ag', 967 'Tuesday': 'Fe', 968 'Wednesday': 'Hg', 969 'Thursday': 'Au', 970 'Friday': 'Cu', 971 'Saturday': 'Pb' 972 }; 973 974 const dayTooltips = { 975 'Sunday': i18n.tooltip_sunday || 'Helium (He) - Second most abundant element in the universe, powers the sun through nuclear fusion', 976 'Monday': i18n.tooltip_monday || 'Silver (Ag) - Precious metal with highest electrical conductivity, reflects light like moonlight', 977 'Tuesday': i18n.tooltip_tuesday || 'Iron (Fe) - Most abundant metal on Earth, essential for steel and human blood', 978 'Wednesday': i18n.tooltip_wednesday || 'Mercury (Hg) - Only metal that is liquid at room temperature, used in thermometers', 979 'Thursday': i18n.tooltip_thursday || 'Gold (Au) - Noble metal that never tarnishes, symbol of value and permanence', 980 'Friday': i18n.tooltip_friday || 'Copper (Cu) - Reddish metal essential for electrical wiring, naturally antibacterial', 981 'Saturday': i18n.tooltip_saturday || 'Lead (Pb) - Dense metal historically used for protection, now known to be toxic' 982 }; 983 984 // Build day sections with translated names 985 englishDays.forEach(englishDay => { 986 const showsForDay = dayGroups[englishDay]; 987 if (showsForDay.length === 0) return; 988 989 const translatedDayName = translatedDays[englishDay] || englishDay; 990 const dayClass = dayClassMap[englishDay] || englishDay.toLowerCase(); 991 const emoji = dayEmojis[englishDay] || 'ð '; 992 const scheduleLabel = i18n.schedule || 'Schedule'; 993 994 const $daySection = $(` 995 <div class="joan-schedule-day-section" data-day="${englishDay}"> 996 <div class="joan-schedule-day-header joan-${dayClass}-header" title="${dayTooltips[englishDay] || ''}"> 997 [${emoji}] ${translatedDayName} ${scheduleLabel} 998 </div> 999 </div> 1000 `); 1001 1002 // Remove emoji if setting is disabled 1003 const showDayEmoji = (typeof joan_ajax !== 'undefined' && joan_ajax.settings && joan_ajax.settings.joan_show_day_emoji === '1'); 1004 if (!showDayEmoji) { 1005 const headerElem = $daySection.find('.joan-schedule-day-header'); 1006 headerElem.removeAttr('title'); 1007 headerElem.text(`${translatedDayName} ${scheduleLabel}`); 1008 } 1009 1010 if (showsForDay.length > 0) { 1011 const $dayTable = $(` 1012 <table class="joan-schedule-day-table joan-${dayClass}-table"> 1013 <thead></thead> 1014 <tbody></tbody> 1015 </table> 1016 `); 1017 1018 const $dayHeader = $headerRow.clone(); 1019 // Only remove Day column if it exists (check if second column is "Day") 1020 const secondHeader = $dayHeader.find('th').eq(1); 1021 if (secondHeader.length && secondHeader.text().trim() === (i18n.day || 'Day')) { 1022 secondHeader.remove(); 1023 } 1024 $dayTable.find('thead').append($dayHeader); 1025 1026 showsForDay.sort((a, b) => { 1027 const timeA = $(a).attr('data-time') || $(a).data('time') || ''; 1028 const timeB = $(b).attr('data-time') || $(b).data('time') || ''; 1029 return timeA.localeCompare(timeB); 1030 }); 1031 1032 showsForDay.forEach($row => { 1033 const $dayRow = $row.clone(); 1034 // Only remove Day cell if the header has a Day column 1035 const hasDayColumn = $dayHeader.find('th').filter(function() { 1036 return $(this).text().trim() === (i18n.day || 'Day'); 1037 }).length > 0; 1038 1039 if (hasDayColumn) { 1040 $dayRow.find('td').eq(1).remove(); 1041 } 1042 $dayTable.find('tbody').append($dayRow); 1043 }); 1044 1045 $daySection.append($dayTable); 1046 } else { 1047 $daySection.append(` 1048 <div class="joan-schedule-empty-day"> 1049 ${i18n.no_shows_scheduled_for || 'No shows scheduled for'} ${translatedDayName} 1050 </div> 1051 `); 1052 } 1053 1054 $container.append($daySection); 1055 }); 1056 1057 // Replace original table 1058 $table.replaceWith($container); 1059 1060 // Apply dark mode only if explicitly enabled 1061 // const isDarkMode = document.body.classList.contains('joan-dark-mode'); 1062 // if (isDarkMode) { 1063 // setTimeout(() => { 1064 // applyDarkMode(true); 1065 1066 // $container.addClass('joan-dark-mode'); 1067 // $container.removeClass('joan-light-mode'); 1068 //}, 100); 1069 //} else { 1070 // Don't force light mode class - let theme styles apply naturally 1071 //$container.removeClass('joan-dark-mode joan-light-mode'); 1072 //} 1073 1074 // Day filtering event handler 1075 $container.find('.joan-day-filter').on('change', function() { 1076 const selectedDay = $(this).val(); 1077 1078 $container.find('.joan-schedule-day-section').each(function() { 1079 const $daySection = $(this); 1080 const dayName = $daySection.data('day'); 1081 1082 if (selectedDay === '' || selectedDay === dayName) { 1083 $daySection.show(); 1084 } else { 1085 $daySection.hide(); 1086 } 1087 }); 1088 }); 1089 }); 1090 } 1091 1092 const initialDelay = typeof rocket_loader !== 'undefined' || window.rocketLoaderDelay ? 2000 : 500; 1093 setTimeout(() => { 1094 initializeDarkMode(); 1095 initializeCacheInvalidation(); 1096 fetchCurrentShow(); 1097 separateScheduleByDays(); 1098 1099 setTimeout(() => { 1100 const isDarkMode = document.body.classList.contains('joan-dark-mode'); 1101 applyDarkMode(isDarkMode); 1102 }, 300); 1103 }, initialDelay); 1104 1105 setInterval(() => { 1106 if (errorCount < maxErrors) { 1107 fetchCurrentShow(true); 1108 } 1109 }, 600000); 1110 1111 let resizeTimeout; 1112 $(window).on('resize', function() { 1113 clearTimeout(resizeTimeout); 1114 resizeTimeout = setTimeout(function() { 1115 if (showCache) { 1116 $('.joan-now-playing').each(function() { 1117 renderShow(showCache, $(this)); 1118 }); 1119 } 1120 }, 100); 1121 }); 1122 1123 window.joanFetchCurrentShow = function() { 1124 errorCount = 0; 1125 clearScheduleCache(); 1126 fetchCurrentShow(true); 1127 }; 1128 1129 window.joanSeparateScheduleByDays = separateScheduleByDays; 1130 1131 window.joanDarkMode = { 1132 initialize: initializeDarkMode, 1133 toggle: toggleDarkMode, 1134 apply: applyDarkMode 1135 }; 1136 1137 window.joanClearCache = clearScheduleCache; 1138 1139 $(document).on('click', '.joan-error a[onclick*="reload"]', function(e) { 1140 e.preventDefault(); 1141 const $container = $(this).closest('.joan-now-playing'); 1142 // TRANSLATION: Use localized string 1143 $container.html('<div class="joan-loading">' + joan_ajax.i18n.refreshing + '</div>'); 1144 window.joanFetchCurrentShow(); 1145 return false;
1146 }); 1147 1148 window.joanStatusUtils = { 1149 applyStatusStyling: applyStatusStyling, 1150 renderStatusMessage: renderStatusMessage 1151 }; 1152 1153 if (typeof rocket_loader !== 'undefined' || window.rocketLoaderDelay) { 1154 console.log('JOAN: Cloudflare Rocket Loader detected, using compatibility mode'); 1155 } 1156}); 1157 1158function initializeLocalTime() { 1159 function updateCurrentTime() { 1160 document.querySelectorAll('.joan-current-time').forEach(function(element) { 1161 const timeSettings = (window.joan_ajax && window.joan_ajax.settings) || {}; 1162 const container = element.closest('.joan-now-playing'); 1163 1164 const hasTimezoneSelector = container && container.querySelector('.joan-timezone-selector'); 1165 if (hasTimezoneSelector) { 1166 element.style.display = 'none'; 1167 return; 1168 } 1169 1170 const allowTimezoneSelector = timeSettings.joan_allow_timezone_selector === '1'; 1171 if (!allowTimezoneSelector) { 1172 element.style.display = 'none'; 1173 return; 1174 } 1175 1176 if (allowTimezoneSelector) { 1177 element.style.display = 'none'; 1178 return; 1179 } 1180 }); 1181 } 1182 1183 updateCurrentTime(); 1184 setInterval(updateCurrentTime, 60000); 1185 document.addEventListener('joanSettingsUpdated', updateCurrentTime); 1186 1187 document.querySelectorAll('.show-time[data-time]').forEach(function(el) { 1188 const time = el.dataset.time; 1189 if (time) { 1190 const timeSettings = window.joan_ajax ? window.joan_ajax.settings : {}; 1191 const timeFormat = timeSettings.time_format || '12'; 1192 1193 let local; 1194 if (timeFormat === '24') { 1195 local = new Date(time).toLocaleTimeString(undefined, { 1196 hour: '2-digit', 1197 minute: '2-digit', 1198 hour12: false, 1199 timeZoneName: 'short' 1200 }); 1201 } else { 1202 local = new Date(time).toLocaleTimeString(undefined, { 1203 hour: 'numeric', 1204 minute: '2-digit', 1205 hour12: true, 1206 timeZoneName: 'short' 1207 }); 1208 } 1209 1210 el.textContent = local; 1211 } 1212 }); 1213} 1214 1215function initializeScheduleHighlight() { 1216 const dayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; 1217 const today = dayNames[new Date().getDay()]; 1218 1219 document.querySelectorAll('.joan-schedule tbody tr').forEach(row => { 1220 const dayCell = row.querySelector('td:nth-child(2)'); 1221 if (dayCell && dayCell.textContent.trim() === today) { 1222 row.classList.add('joan-today-highlight'); 1223 } 1224 }); 1225 1226 document.querySelectorAll('.joan-schedule-day-section').forEach(section => { 1227 if (section.dataset.day === today) { 1228 section.classList.add('joan-today-highlight'); 1229 } 1230 }); 1231 1232 document.querySelectorAll('.joan-day-filter').forEach(filter => { 1233 filter.addEventListener('change', function () { 1234 const selected = this.value; 1235 const parentTable = this.closest('.joan-schedule-container')?.querySelector('.joan-schedule') || 1236 document.querySelector('.joan-schedule'); 1237 1238 if (parentTable) { 1239 parentTable.querySelectorAll('tbody tr').forEach(row => { 1240 const day = row.querySelector('td:nth-child(2)')?.textContent.trim(); 1241 row.style.display = (selected === '' || selected === day) ? '' : 'none'; 1242 }); 1243 } 1244 }); 1245 }); 1246} 1247 1248if (document.readyState === 'loading') { 1249 document.addEventListener('DOMContentLoaded', initializeLocalTime); 1250 document.addEventListener('DOMContentLoaded', initializeScheduleHighlight); 1251} else { 1252 setTimeout(initializeLocalTime, typeof rocket_loader !== 'undefined' ? 1500 : 100); 1253 setTimeout(initializeScheduleHighlight, typeof rocket_loader !== 'undefined' ? 1500 : 100); 1254} 1255 1256function initializePageBuilders() { 1257 function initElementorIntegration() { 1258 if (window.elementorFrontend && window.elementorFrontend.hooks && window.elementorFrontend.hooks.addAction) { 1259 elementorFrontend.hooks.addAction('frontend/element_ready/global', function($scope) { 1260 if ($scope.find('.joan-now-playing').length) { 1261 if (window.joanFetchCurrentShow) { 1262 setTimeout(window.joanFetchCurrentShow, 500); 1263 } 1264 } 1265 1266 if ($scope.find('table.joan-schedule').length) { 1267 if (window.joanSeparateScheduleByDays) { 1268 setTimeout(window.joanSeparateScheduleByDays, 500); 1269 } 1270 } 1271 }); 1272 } 1273 } 1274 1275 function initVCIntegration() { 1276 if (window.vc_js && typeof jQuery !== 'undefined') { 1277 jQuery(document).on('vc_reload', function() { 1278 setTimeout(function() { 1279 if (window.joanFetchCurrentShow) { 1280 window.joanFetchCurrentShow(); 1281 } 1282 if (window.joanSeparateScheduleByDays) { 1283 window.joanSeparateScheduleByDays(); 1284 } 1285 if (window.joanDarkMode) { 1286 window.joanDarkMode.initialize(); 1287 } 1288 }, 500); 1289 }); 1290 } 1291 } 1292 1293 try { 1294 initElementorIntegration(); 1295 initVCIntegration(); 1296 } catch (error) { 1297 console.log('JOAN: Page builder integration not available:', error.message); 1298 } 1299
1300 let checkCount = 0; 1301 const checkInterval = setInterval(() => { 1302 checkCount++; 1303 1304 try { 1305 if (window.elementorFrontend && !window.joanElementorInit) { 1306 initElementorIntegration(); 1307 window.joanElementorInit = true; 1308 } 1309 1310 if (window.vc_js && !window.joanVCInit) { 1311 initVCIntegration(); 1312 window.joanVCInit = true; 1313 } 1314 } catch (error) { 1315 // Silent 1316 } 1317 1318 if (checkCount > 10) { 1319 clearInterval(checkInterval); 1320 } 1321 }, 500); 1322} 1323 1324if (document.readyState === 'loading') { 1325 document.addEventListener('DOMContentLoaded', function() { 1326 setTimeout(initializePageBuilders, typeof rocket_loader !== 'undefined' ? 2000 : 1000); 1327 }); 1328} else { 1329 setTimeout(initializePageBuilders, typeof rocket_loader !== 'undefined' ? 2500 : 1000); 1330} 1331 1332// ============================================================================ 1333// WPBAKERY DYNAMIC CONTENT FIX - v6.0.9 1334// Ensure dropdowns are visible using CSS classes (no inline styles) 1335// ============================================================================ 1336(function() { 1337 if (typeof jQuery === 'undefined') return; 1338 1339 const $ = jQuery; 1340 1341 // Simple check to ensure elements are shown (uses classes only) 1342 let checkCount = 0; 1343 const checkInterval = setInterval(function() { 1344 checkCount++; 1345 1346 // Add a class to force visibility if needed 1347 $('.joan-timezone-selector:hidden').addClass('joan-force-visible').show(); 1348 $('.joan-schedule-controls:hidden').addClass('joan-force-visible').show(); 1349 1350 if (checkCount >= 10) { 1351 clearInterval(checkInterval); 1352 } 1353 }, 1000); 1354 1355 console.log('JOAN v6.0.9: WPBakery dropdown visibility check active'); 1356})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.