您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a slideshow feature to Wyze Events page with keyboard navigation, playback speed control, interactive timeline, and more
// ==UserScript== // @name Wyze Thumbnail Slideshow // @namespace http://ptelectronics.net // @version 1.9.14 // @description Adds a slideshow feature to Wyze Events page with keyboard navigation, playback speed control, interactive timeline, and more // @author Math Shamenson // @match https://my.wyze.com/events* // @grant GM_xmlhttpRequest // @connect wyze-event-streaming-prod.a.momentohq.com // @license MIT // @run-at document-idle // @supportURL https://greasyfork.org/scripts/SCRIPT_ID/feedback // @homepageURL https://greasyfork.org/scripts/SCRIPT_ID // ==/UserScript== (function() { 'use strict'; // 1. State variables // 2. Logger // 3. Core slideshow logic // 4. UI initialization and styles // 5. Controls and features // 6. Memory management // 7. Initialization system // End of IIFE // ------------------------------ // SHARED SLIDESHOW STATE // ------------------------------ let thumbnails = []; // Array of objects: { src, videoSrc } let currentIndex = 0; let interval = null; let isPlaying = false; let speed = 1000; // Default: 1s const MIN_SPEED = 200; // Min speed (0.2s) const MAX_SPEED = 5000; // Max speed (5s) let slideshowInitialized = false; // For lazy-loading IntersectionObserver let thumbnailObserver = null; // -------------------------------- // LOGGER // -------------------------------- const Logger = { levels: { ERROR: 'ERROR', WARN: 'WARN', INFO: 'INFO', DEBUG: 'DEBUG' }, log(level, message, error = null) { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] [${level}] ${message}`; if (localStorage.getItem('wyzeSlideshow_debug') === 'false') { console.log(logMessage); if (error) console.error(error); } if (!window.wyzeSlideshowLogs) window.wyzeSlideshowLogs = []; window.wyzeSlideshowLogs.push({ timestamp, level, message, error }); if (window.wyzeSlideshowLogs.length > 1000) { window.wyzeSlideshowLogs.shift(); } } }; // DOM Observer Functions function observeDOMChanges() { let debounceTimeout; const observer = new MutationObserver((mutationsList) => { Logger.log(Logger.levels.DEBUG, `MutationObserver saw ${mutationsList.length} mutations.`); clearTimeout(debounceTimeout); debounceTimeout = setTimeout(() => { collectThumbnailsAndUpdateUI(); }, 300); }); observer.observe(document.body, { childList: true, subtree: true }); Logger.log(Logger.levels.INFO, 'MutationObserver initialized.'); } // Lazy Loading Functions function improvedThumbnailCollection() { const observerOptions = { root: null, rootMargin: '50px', threshold: 0.1 }; thumbnailObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; if (img.dataset.src) { img.src = img.dataset.src; img.removeAttribute('data-src'); Logger.log(Logger.levels.DEBUG, `Lazy-loaded thumbnail: ${img.alt}`); } thumbnailObserver.unobserve(img); } }); }, observerOptions); Logger.log(Logger.levels.INFO, 'IntersectionObserver for lazy-loading initialized.'); } // Set playback speed (referenced in keyboard controls) function setPlaybackSpeed(multiplier) { if (multiplier === 1) { speed = 1000; } else { speed = Math.floor(1000 / multiplier); } speed = Math.max(MIN_SPEED, Math.min(speed, MAX_SPEED)); if (isPlaying) { pauseSlideshow(); startSlideshowInterval(); } updateSpeedDisplay(); Logger.log(Logger.levels.INFO, `Playback speed set via multiplier (${multiplier}). New speed: ${speed}ms.`); } // Jump to percentage (used in timeline navigation) function jumpToPercentage(percent) { if (thumbnails.length === 0) return; const targetIndex = Math.floor((percent / 100) * thumbnails.length); currentIndex = Math.min(targetIndex, thumbnails.length - 1); updateSlideshow(); Logger.log(Logger.levels.INFO, `Jumped to ${percent}% (index: ${currentIndex})`); } // Show/hide thumbnail preview functions function showThumbnailPreview(index, x, y) { if (!thumbnails[index]) return; let preview = document.getElementById('timeline-preview'); if (!preview) { preview = document.createElement('div'); preview.id = 'timeline-preview'; preview.style.cssText = ` position: fixed; background: white; padding: 5px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); z-index: 10004; pointer-events: none; `; document.body.appendChild(preview); } const img = new Image(); img.src = thumbnails[index].src; img.style.maxWidth = '200px'; img.style.maxHeight = '150px'; preview.innerHTML = ''; preview.appendChild(img); preview.style.left = `${x - preview.offsetWidth/2}px`; preview.style.top = `${y - preview.offsetHeight - 10}px`; preview.style.display = 'block'; } function hideThumbnailPreview() { const preview = document.getElementById('timeline-preview'); if (preview) { preview.style.display = 'none'; } } // Button Management function addStartButton() { let btn = document.getElementById('start-slideshow-btn'); if (!btn) { btn = document.createElement('button'); btn.id = 'start-slideshow-btn'; btn.textContent = 'Start Slideshow'; btn.className = 'slideshow-button'; btn.onclick = startSlideshow; document.body.appendChild(btn); Logger.log(Logger.levels.INFO, 'Start Slideshow button added.'); } else { btn.style.display = 'block'; Logger.log(Logger.levels.DEBUG, 'Start Slideshow button already exists, now ensured visible.'); } } // Close functions function closeSlideshow() { pauseSlideshow(); const container = document.getElementById('slideshow-container'); if (container) { container.style.display = 'none'; Logger.log(Logger.levels.INFO, 'Slideshow closed.'); } } // Timeline management functions function handleKeyboard(e) { const container = document.getElementById('slideshow-container'); if (!container || container.style.display === 'none') return; switch (e.key) { case 'ArrowLeft': prevImage(); break; case 'ArrowRight': nextImage(); break; case ' ': e.preventDefault(); togglePlayPause(); break; case 'Escape': closeSlideshow(); break; case '+': case '=': increaseSpeed(); break; case '-': decreaseSpeed(); break; default: break; } } function togglePlayPause() { if (isPlaying) { pauseSlideshow(); Logger.log(Logger.levels.INFO, 'Slideshow paused.'); } else { startSlideshowInterval(); Logger.log(Logger.levels.INFO, 'Slideshow playing.'); } } // Memory management function cleanupUnusedThumbnails() { const MAX_CACHED_THUMBNAILS = 50; if (thumbnails.length > MAX_CACHED_THUMBNAILS) { const excess = thumbnails.length - MAX_CACHED_THUMBNAILS; const removed = thumbnails.splice(MAX_CACHED_THUMBNAILS, excess); Logger.log(Logger.levels.INFO, `Cleaned up ${removed.length} excess thumbnails from memory.`); // Clean up DOM thumbnails const unusedThumbs = document.querySelectorAll('.slideshow-thumbnail:not(.active)'); unusedThumbs.forEach((thumb) => { thumb.src = ''; thumb.remove(); Logger.log(Logger.levels.DEBUG, 'Removed unused thumbnail from DOM.'); }); } } // -------------------------------- // CORE SLIDESHOW LOGIC // -------------------------------- function startSlideshow() { Logger.log(Logger.levels.INFO, 'Starting slideshow...'); collectThumbnailsAndUpdateUI(); const container = document.getElementById('slideshow-container'); if (!container) { Logger.log(Logger.levels.ERROR, 'Slideshow container not found.'); return; } if (thumbnails.length > 0) { currentIndex = 0; updateSlideshow(); container.style.display = 'flex'; togglePlayPause(); // Start playing by default Logger.log(Logger.levels.INFO, 'Slideshow started.'); } else { notifyNoThumbnails(); Logger.log(Logger.levels.WARN, 'No thumbnails to display.'); } } function updateSlideshow() { const container = document.getElementById('slideshow-container'); const link = document.getElementById('slideshow-link'); const img = document.getElementById('slideshow-image'); if (!container || !img || thumbnails.length === 0) { Logger.log(Logger.levels.WARN, 'No container/image or thumbnails available for slideshow.'); return; } currentIndex = (currentIndex + thumbnails.length) % thumbnails.length; const currentThumbnail = thumbnails[currentIndex]; if (currentThumbnail) { if (currentThumbnail.needsRotation) { img.classList.add('doorbell-orientation'); link.classList.add('doorbell-container'); container.classList.add('has-rotated-image'); } else { img.classList.remove('doorbell-orientation'); link.classList.remove('doorbell-container'); container.classList.remove('has-rotated-image'); } img.src = currentThumbnail.src; if (link) { link.onclick = (e) => { e.preventDefault(); playVideo(currentThumbnail.videoSrc); }; link.href = '#'; } Logger.log(Logger.levels.INFO, `Displaying thumbnail ${currentIndex + 1}/${thumbnails.length} (rotation: ${currentThumbnail.needsRotation})`); updateProgressBar(); } } // [Previous functions through createHelpOverlay() remain unchanged] // -------------------------------- // COLLECT THUMBNAILS // -------------------------------- /* function collectThumbnails() { thumbnails = []; const eventDivs = document.querySelectorAll('div[id^="event_"]'); Logger.log(Logger.levels.INFO, `Found ${eventDivs.length} event containers`); eventDivs.forEach(eventDiv => { const imgWrapper = eventDiv.querySelector('.VideoWrap img'); if (imgWrapper) { const src = imgWrapper.dataset.src || imgWrapper.src; // Extract event ID using updated pattern for Wyze's current system const match = src.match(/wyze-thumbnail-service-prod\/(.+?)_\d+\.jpg/); if (match) { const eventId = match[1]; // Construct video URL using Wyze's streaming service endpoint const videoSrc = `https://wyze-event-streaming-prod.a.momentohq.com/cache/wyze-streaming-service-prod/${eventId}.mp4`; thumbnails.push({ src, videoSrc }); Logger.log(Logger.levels.DEBUG, `Added thumbnail for event ${eventId}`); } else { Logger.log(Logger.levels.DEBUG, `Could not parse event ID from thumbnail URL: ${src}`); } } }); Logger.log(Logger.levels.INFO, `Collected ${thumbnails.length} thumbnails`); updateStartButtonVisibility(); } */ function collectThumbnails() { thumbnails = []; const eventDivs = document.querySelectorAll('div[id^="event_"]'); Logger.log(Logger.levels.INFO, `Found ${eventDivs.length} event containers`); eventDivs.forEach(eventDiv => { const imgWrapper = eventDiv.querySelector('.VideoWrap img'); if (imgWrapper) { const src = imgWrapper.dataset.src || imgWrapper.src; if (!src) { Logger.log(Logger.levels.DEBUG, 'No src found for image wrapper'); return; } // Check if this is a doorbell camera image that needs rotation const needsRotation = src.includes('wyze-device-alarm-file-face.s3.us-west-2.amazonaws.com'); let eventId = null; let videoSrc = null; // Multiple pattern matching let match = src.match(/wyze-thumbnail-service-prod\/(.+?)_\d+\.jpg/) || src.match(/cp-t-usw2\.s3[^\/]*\/([^_]+)_\d+\.jpg/) || src.match(/momentohq\.com\/([^_]+)_\d+\.jpg/) || src.match(/([a-f0-9-]{36})/i) || src.match(/(\w+)\/\d{4}-\d{2}-\d{2}\/(\w+)/); // New pattern for alarm files if (match) { eventId = match[1]; videoSrc = `https://wyze-event-streaming-prod.a.momentohq.com/cache/wyze-streaming-service-prod/${eventId}.mp4`; thumbnails.push({ src, videoSrc, eventId, needsRotation: needsRotation, timestamp: eventDiv.getAttribute('data-timestamp') || null }); Logger.log(Logger.levels.DEBUG, `Added thumbnail for event ${eventId} (rotation: ${needsRotation}), src: ${src}`); } } }); // Sort thumbnails by timestamp if available if (thumbnails.length > 0 && thumbnails[0].timestamp) { thumbnails.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); } Logger.log(Logger.levels.INFO, `Collected ${thumbnails.length} thumbnails`); updateStartButtonVisibility(); } // -------------------------------- // VIDEO PLAYER CONTROL // -------------------------------- function playVideo(videoSrc) { const videoPlayer = document.querySelector('video.vjs-tech'); const spinner = document.getElementById('loading-spinner'); if (!videoPlayer) { Logger.log(Logger.levels.ERROR, 'Video player element not found.'); notifyVideoPlayerNotFound(); return; } if (spinner) spinner.style.display = 'block'; // Using GM_xmlhttpRequest for cross-origin video requests GM_xmlhttpRequest({ method: 'GET', url: videoSrc, responseType: 'blob', onload: function(response) { const blobUrl = URL.createObjectURL(response.response); videoPlayer.pause(); videoPlayer.src = blobUrl; videoPlayer.load(); videoPlayer.play() .then(() => { Logger.log(Logger.levels.INFO, `Playing video: ${videoSrc}`); if (spinner) spinner.style.display = 'none'; }) .catch(error => { Logger.log(Logger.levels.ERROR, `Error playing video: ${videoSrc}`, error); notifyVideoError(); if (spinner) spinner.style.display = 'none'; }); videoPlayer.addEventListener('ended', () => { URL.revokeObjectURL(blobUrl); }, { once: true }); }, onerror: function(error) { Logger.log(Logger.levels.ERROR, `Failed to fetch video: ${videoSrc}`, error); notifyVideoError(); if (spinner) spinner.style.display = 'none'; } }); } // After the Logger object but before UI initialization: function collectThumbnailsAndUpdateUI() { collectThumbnails(); updateSlideshowUI(); } function updateSlideshowUI() { const container = document.getElementById('slideshow-container'); if (!container) return; updateProgressBar(); } function preloadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error(`Failed to load image: ${src}`)); img.src = src; Logger.log(Logger.levels.DEBUG, `Preloading image: ${src}`); }); } // Navigation Functions function prevImage() { pauseSlideshow(); currentIndex = (currentIndex - 1 + thumbnails.length) % thumbnails.length; Logger.log(Logger.levels.DEBUG, 'Navigating to previous image.'); updateSlideshow(); } function nextImage() { pauseSlideshow(); currentIndex = (currentIndex + 1) % thumbnails.length; Logger.log(Logger.levels.DEBUG, 'Navigating to next image.'); updateSlideshow(); } function startSlideshowInterval() { isPlaying = true; // Start auto-play with dynamic thumbnail updates interval = setInterval(() => { // Refresh thumbnails dynamically collectThumbnails(); // Ensure the index stays within bounds if (currentIndex >= thumbnails.length) { currentIndex = 0; // Loop back to the start if out of bounds } updateSlideshow(); // Increment to the next slide currentIndex = (currentIndex + 1) % thumbnails.length; }, speed); Logger.log(Logger.levels.INFO, `Slideshow interval started (speed: ${speed}ms).`); } function pauseSlideshow() { isPlaying = false; if (interval) { clearInterval(interval); interval = null; Logger.log(Logger.levels.DEBUG, 'Slideshow interval cleared.'); } } // Speed Control Functions function increaseSpeed() { speed = Math.max(MIN_SPEED, speed - 200); if (isPlaying) { pauseSlideshow(); startSlideshowInterval(); } updateSpeedDisplay(); Logger.log(Logger.levels.INFO, `Playback speed increased to ${speed}ms`); } function decreaseSpeed() { speed = Math.min(MAX_SPEED, speed + 200); if (isPlaying) { pauseSlideshow(); startSlideshowInterval(); } updateSpeedDisplay(); Logger.log(Logger.levels.INFO, `Playback speed decreased to ${speed}ms`); } function resetSpeed() { speed = 1000; if (isPlaying) { pauseSlideshow(); startSlideshowInterval(); } updateSpeedDisplay(); Logger.log(Logger.levels.INFO, 'Playback speed reset to default'); } function updateSpeedDisplay() { const speedDisplay = document.getElementById('speed-display'); if (speedDisplay) { speedDisplay.textContent = `Speed: ${speed} ms`; } } // Notification Functions function notifyNoThumbnails() { const el = document.getElementById('no-thumbnails-notification'); if (el) { el.style.display = 'block'; setTimeout(() => { el.style.display = 'none'; }, 3000); Logger.log(Logger.levels.INFO, 'Displayed "No thumbnails" notification'); } } function notifyVideoError() { const el = document.getElementById('video-error-notification'); if (el) { el.style.display = 'block'; setTimeout(() => { el.style.display = 'none'; }, 3000); Logger.log(Logger.levels.INFO, 'Displayed "Video error" notification'); } } function notifyVideoPlayerNotFound() { const el = document.getElementById('video-player-not-found-notification'); if (el) { el.style.display = 'block'; setTimeout(() => { el.style.display = 'none'; }, 3000); Logger.log(Logger.levels.INFO, 'Displayed "Video player not found" notification'); } } // Display Control Functions function toggleFullscreen() { const container = document.getElementById('slideshow-container'); if (!container) return; try { if (!document.fullscreenElement) { container.requestFullscreen(); Logger.log(Logger.levels.INFO, 'Entered fullscreen mode'); } else { document.exitFullscreen(); Logger.log(Logger.levels.INFO, 'Exited fullscreen mode'); } } catch (error) { Logger.log(Logger.levels.ERROR, 'Fullscreen toggle failed', error); } } function toggleMute() { const videoPlayer = document.querySelector('video.vjs-tech'); if (videoPlayer) { videoPlayer.muted = !videoPlayer.muted; Logger.log(Logger.levels.INFO, `Video ${videoPlayer.muted ? 'muted' : 'unmuted'}`); } } function toggleHelpOverlay() { const helpOverlay = document.getElementById('slideshow-help-overlay'); if (helpOverlay) { const isHidden = helpOverlay.style.display === 'none' || !helpOverlay.style.display; helpOverlay.style.display = isHidden ? 'block' : 'none'; Logger.log(Logger.levels.INFO, `Help overlay ${isHidden ? 'shown' : 'hidden'}`); } } function updateStartButtonVisibility() { const startBtn = document.getElementById('start-slideshow-btn'); if (startBtn) { startBtn.style.display = thumbnails.length > 0 ? 'block' : 'none'; Logger.log(Logger.levels.DEBUG, `Start button visibility updated: ${thumbnails.length > 0 ? 'visible' : 'hidden'}`); } } function updateProgressBar() { const progress = document.getElementById('progress'); if (!progress) { Logger.log(Logger.levels.DEBUG, 'No progress bar found.'); return; } if (thumbnails.length <= 1) { progress.style.width = '100%'; return; } const percent = ((currentIndex + 1) / thumbnails.length) * 100; progress.style.width = percent + '%'; Logger.log(Logger.levels.DEBUG, `Progress bar updated to ${percent}%`); } function handleButtonClick(label) { Logger.log(Logger.levels.DEBUG, `Button clicked: ${label}`); switch (label) { case 'Prev': prevImage(); break; case 'Play/Pause': togglePlayPause(); break; case 'Next': nextImage(); break; case 'Faster': increaseSpeed(); break; case 'Slower': decreaseSpeed(); break; case 'Reset Speed': resetSpeed(); break; case 'Close': closeSlideshow(); break; default: Logger.log(Logger.levels.WARN, `Unknown button label: ${label}`); break; } } // -------------------------------- // UI INITIALIZATION // -------------------------------- function injectOrientationStyles() { const style = document.createElement('style'); style.textContent = ` .doorbell-orientation { transform: rotate(90deg); transform-origin: center center; width: auto; height: auto; max-width: 80vh; /* Use viewport height for better scaling */ max-height: 80vw; /* Use viewport width for better scaling */ object-fit: contain; margin: auto; } .doorbell-container { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; padding: 2rem; box-sizing: border-box; } /* Ensure the slideshow container can accommodate rotated images */ #slideshow-container.has-rotated-image { display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden; } `; document.head.appendChild(style); } function injectStyles() { const style = document.createElement('style'); style.textContent = ` #slideshow-container { position: fixed; top: 5%; left: 5%; width: 90%; height: 90%; background: rgba(0, 0, 0, 0.95); color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 10000; border-radius: 10px; border: 2px solid #fff; overflow: hidden; } #slideshow-link { display: block; max-width: 95%; max-height: 75%; cursor: pointer; text-align: center; } #slideshow-image { max-width: 100%; max-height: 100%; border-radius: 5px; } #progress-bar { width: 90%; height: 30px; background: gray; margin-top: 10px; border-radius: 10px; cursor: pointer; position: relative; } #progress { height: 100%; background: limegreen; width: 0%; border-radius: 10px; transition: width 0.5s ease; } .slideshow-button, .speed-button { margin: 5px; padding: 10px 15px; border: none; border-radius: 5px; background-color: #007BFF; color: white; font-size: 16px; cursor: pointer; } .slideshow-button:hover, .speed-button:hover { background-color: #0056b3; } #speed-display { margin-top: 10px; font-size: 16px; } #start-slideshow-btn { position: fixed; bottom: 10px; right: 150px; z-index: 10001; padding: 10px 15px; border: none; border-radius: 5px; background-color: #28a745; color: white; font-size: 16px; cursor: pointer; } /* Enhanced visibility for the start button */ #start-slideshow-btn:hover { background-color: #218838; transform: scale(1.05); transition: all 0.2s ease; } .slideshow-thumbnail.active { border: 3px solid #007BFF; border-radius: 5px; } /* Improved loading spinner visibility */ #loading-spinner { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: none; z-index: 10002; background: rgba(0, 0, 0, 0.7); padding: 20px; border-radius: 10px; } /* Enhanced notification styling */ #no-thumbnails-notification, #video-error-notification, #video-player-not-found-notification { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255, 0, 0, 0.8); padding: 20px; border-radius: 5px; z-index: 10003; color: white; font-size: 18px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.3); } /* Accessibility improvements */ .slideshow-button:focus, .slideshow-thumbnail:focus { outline: 3px solid #007BFF; outline-offset: 2px; box-shadow: 0 0 5px rgba(0,123,255,0.5); } /* Help overlay styling */ #slideshow-help-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.9); z-index: 10001; display: none; } .help-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 10px; color: black; max-width: 80%; max-height: 80%; overflow-y: auto; } .help-content h3 { margin-top: 0; color: #007BFF; } .help-content ul { list-style-type: none; padding: 0; } .help-content li { margin: 10px 0; padding: 5px 0; border-bottom: 1px solid #eee; } `; document.head.appendChild(style); Logger.log(Logger.levels.INFO, 'Enhanced styles injected.'); } // Enhanced UI creation with better error handling function createSlideshowUI() { const container = document.createElement('div'); container.id = 'slideshow-container'; container.style.display = 'none'; // Link & main image with improved error handling const link = document.createElement('a'); link.id = 'slideshow-link'; link.href = '#'; const img = document.createElement('img'); img.id = 'slideshow-image'; img.onerror = () => { Logger.log(Logger.levels.ERROR, 'Failed to load image'); img.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="%23ccc"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="%23666">Error</text></svg>'; }; link.appendChild(img); container.appendChild(link); // Improved spinner with better visibility const spinner = document.createElement('div'); spinner.id = 'loading-spinner'; spinner.innerHTML = ` <div style="text-align: center;"> <img src="https://i.imgur.com/llF5iyg.gif" alt="Loading..." width="50" height="50"> <div style="margin-top: 10px; color: white;">Loading video...</div> </div> `; container.appendChild(spinner); // Enhanced progress bar with better visual feedback const progressBar = document.createElement('div'); progressBar.id = 'progress-bar'; const progress = document.createElement('div'); progress.id = 'progress'; progressBar.appendChild(progress); container.appendChild(progressBar); // Improved control buttons with better accessibility const controlContainer = document.createElement('div'); controlContainer.style.marginTop = '10px'; controlContainer.style.display = 'flex'; controlContainer.style.flexWrap = 'wrap'; controlContainer.style.justifyContent = 'center'; const controls = [ { label: 'Prev', icon: '⏮' }, { label: 'Play/Pause', icon: '⏯' }, { label: 'Next', icon: '⏭' }, { label: 'Faster', icon: '⚡' }, { label: 'Slower', icon: '🐢' }, { label: 'Reset Speed', icon: '⟲' }, { label: 'Close', icon: '✖' } ]; controls.forEach(({ label, icon }) => { const btn = document.createElement('button'); btn.textContent = `${icon} ${label}`; btn.className = 'slideshow-button'; btn.setAttribute('aria-label', label); btn.onclick = () => handleButtonClick(label); controlContainer.appendChild(btn); }); container.appendChild(controlContainer); // Enhanced thumbnail preview container const thumbnailPreviewContainer = document.createElement('div'); thumbnailPreviewContainer.id = 'thumbnail-preview-container'; thumbnailPreviewContainer.style.display = 'flex'; thumbnailPreviewContainer.style.flexWrap = 'wrap'; thumbnailPreviewContainer.style.marginTop = '10px'; container.appendChild(thumbnailPreviewContainer); // Improved speed display with better visibility const speedDisplay = document.createElement('div'); speedDisplay.id = 'speed-display'; speedDisplay.textContent = `Speed: ${speed} ms`; container.appendChild(speedDisplay); // Enhanced notifications const notifications = [ { id: 'no-thumbnails-notification', text: 'No thumbnails found to start the slideshow.' }, { id: 'video-error-notification', text: 'Failed to play the selected video.' }, { id: 'video-player-not-found-notification', text: 'Video player not found on the page.' } ]; notifications.forEach(({ id, text }) => { const notification = document.createElement('div'); notification.id = id; notification.style.display = 'none'; notification.textContent = text; container.appendChild(notification); }); document.body.appendChild(container); // Enhanced progress bar interaction progressBar.addEventListener('click', (e) => { const rect = progressBar.getBoundingClientRect(); const percent = (e.clientX - rect.left) / rect.width; currentIndex = Math.floor(percent * thumbnails.length); updateSlideshow(); pauseSlideshow(); Logger.log(Logger.levels.INFO, `Scrubbed to index ${currentIndex + 1}/${thumbnails.length}`); }); // Enhanced keyboard controls window.addEventListener('keydown', handleKeyboard); Logger.log(Logger.levels.INFO, 'Enhanced slideshow UI created.'); } // -------------------------------- // ENHANCED KEYBOARD AND TOUCH CONTROLS // -------------------------------- /* const touchControls = { initialize() { const container = document.getElementById('slideshow-container'); if (!container) return; // Track touch positions for gesture detection let touchStartX = 0; let touchEndX = 0; let touchStartY = 0; let touchEndY = 0; let touchStartTime = 0; container.addEventListener('touchstart', (e) => { // Store initial touch coordinates and time touchStartX = e.changedTouches[0].screenX; touchStartY = e.changedTouches[0].screenY; touchStartTime = Date.now(); }); container.addEventListener('touchend', (e) => { // Calculate touch movement and duration touchEndX = e.changedTouches[0].screenX; touchEndY = e.changedTouches[0].screenY; const touchDuration = Date.now() - touchStartTime; handleGesture(); }); function handleGesture() { const SWIPE_THRESHOLD = 50; // Minimum distance for swipe const SWIPE_TIME_LIMIT = 300; // Maximum time for swipe (ms) const touchDuration = Date.now() - touchStartTime; const swipeDistanceX = touchEndX - touchStartX; const swipeDistanceY = touchEndY - touchStartY; // Only process quick, intentional swipes if (touchDuration <= SWIPE_TIME_LIMIT) { if (Math.abs(swipeDistanceX) > SWIPE_THRESHOLD && Math.abs(swipeDistanceX) > Math.abs(swipeDistanceY)) { if (swipeDistanceX > 0) { prevImage(); Logger.log(Logger.levels.INFO, 'Swiped right => Previous image'); } else { nextImage(); Logger.log(Logger.levels.INFO, 'Swiped left => Next image'); } } } } } }; */ // -------------------------------- // ENHANCED TIMELINE NAVIGATION // -------------------------------- const timelineNavigation = { initialize() { const progressBar = document.getElementById('progress-bar'); if (!progressBar) return; let isDragging = false; let wasPlaying = false; // Enhanced drag handling with play state management progressBar.addEventListener('mousedown', (e) => { isDragging = true; wasPlaying = isPlaying; pauseSlideshow(); updateTimelinePosition(e); Logger.log(Logger.levels.DEBUG, 'Started dragging progress bar'); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; updateTimelinePosition(e); }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; // Restore previous play state if was playing if (wasPlaying) { startSlideshowInterval(); } Logger.log(Logger.levels.DEBUG, 'Stopped dragging progress bar'); } }); function updateTimelinePosition(e) { const progressBar = document.getElementById('progress-bar'); if (!progressBar) return; const rect = progressBar.getBoundingClientRect(); const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const targetIndex = Math.floor(percent * thumbnails.length); // Preload the thumbnail for the target index if not already loaded if (thumbnails[targetIndex] && !thumbnails[targetIndex].loaded) { preloadImage(thumbnails[targetIndex].src).then(() => { thumbnails[targetIndex].loaded = true; Logger.log(Logger.levels.DEBUG, `Preloaded thumbnail for scrubbing: ${thumbnails[targetIndex].src}`); }).catch((error) => { Logger.log(Logger.levels.ERROR, `Failed to preload thumbnail for scrubbing: ${error.message}`); }); } // Update current index and slideshow currentIndex = targetIndex; updateSlideshow(); } // Add hover preview functionality let previewTimeout; progressBar.addEventListener('mousemove', (e) => { clearTimeout(previewTimeout); previewTimeout = setTimeout(() => { if (!isDragging) { const rect = progressBar.getBoundingClientRect(); const percent = (e.clientX - rect.left) / rect.width; const previewIndex = Math.floor(percent * thumbnails.length); showThumbnailPreview(previewIndex, e.clientX, e.clientY); } }, 100); }); progressBar.addEventListener('mouseleave', () => { clearTimeout(previewTimeout); hideThumbnailPreview(); }); } }; // -------------------------------- // ENHANCED KEYBOARD SHORTCUTS // -------------------------------- const enhancedKeyboardControls = { initialize() { // Map of keyboard shortcuts to their actions const shortcuts = { 'f': () => toggleFullscreen(), 'm': () => toggleMute(), '1': () => setPlaybackSpeed(1), '2': () => setPlaybackSpeed(2), '3': () => setPlaybackSpeed(0.5), 'h': () => toggleHelpOverlay(), // Number keys 1-9 for quick navigation ...[...Array(9)].reduce((acc, _, i) => ({ ...acc, [`${i + 1}`]: () => jumpToPercentage((i + 1) * 10) }), {}) }; document.addEventListener('keydown', (e) => { // Only process shortcuts if slideshow is visible const container = document.getElementById('slideshow-container'); if (!container || container.style.display === 'none') return; if (shortcuts[e.key]) { e.preventDefault(); shortcuts[e.key](); Logger.log(Logger.levels.INFO, `Keyboard shortcut: ${e.key}`); } }); } }; // -------------------------------- // ENHANCED HELP OVERLAY SYSTEM // -------------------------------- function createHelpOverlay() { const helpOverlay = document.createElement('div'); helpOverlay.id = 'slideshow-help-overlay'; // Creating a more comprehensive and organized help content helpOverlay.innerHTML = ` <div class="help-content"> <h3>Wyze Slideshow Controls</h3> <div style="margin-bottom: 20px;"> <h4>Navigation</h4> <ul> <li>←/→ : Navigate between images</li> <li>Space : Play/Pause slideshow</li> <li>1-9 : Jump to percentage through slideshow (1=10%, 9=90%)</li> <li>Esc : Close slideshow</li> </ul> </div> <div style="margin-bottom: 20px;"> <h4>Playback Control</h4> <ul> <li>+ / = : Increase speed</li> <li>- : Decrease speed</li> <li>R : Reset speed to default</li> <li>M : Toggle video mute</li> </ul> </div> <div style="margin-bottom: 20px;"> <h4>Display Options</h4> <ul> <li>F : Toggle fullscreen</li> <li>H : Toggle this help overlay</li> </ul> </div> <div style="margin-bottom: 20px;"> <h4>Touch Controls</h4> <ul> <li>Swipe Left : Next image</li> <li>Swipe Right : Previous image</li> <li>Double Tap : Play/Pause</li> </ul> </div> <div> <h4>Timeline Navigation</h4> <ul> <li>Click or drag the progress bar to navigate</li> <li>Hover over progress bar to preview thumbnails</li> </ul> </div> </div> `; // Add click handler to close help overlay when clicking outside content helpOverlay.addEventListener('click', (e) => { if (e.target === helpOverlay) { toggleHelpOverlay(); } }); document.body.appendChild(helpOverlay); Logger.log(Logger.levels.INFO, 'Enhanced help overlay created'); } // -------------------------------- // ACCESSIBILITY ENHANCEMENTS // -------------------------------- const accessibilityEnhancements = { initialize() { const container = document.getElementById('slideshow-container'); if (!container) return; // Add ARIA attributes for better screen reader support container.setAttribute('role', 'region'); container.setAttribute('aria-label', 'Thumbnail Slideshow'); // Enhance keyboard navigation this.setupKeyboardNav(); // Add announcements for screen readers this.setupLiveRegion(); // Enhance button labels this.enhanceButtonLabels(); Logger.log(Logger.levels.INFO, 'Accessibility enhancements initialized'); }, setupKeyboardNav() { // Add tabindex to make elements focusable const focusableElements = document.querySelectorAll('.slideshow-button, .slideshow-thumbnail'); focusableElements.forEach(el => { el.setAttribute('tabindex', '0'); }); // Add key handlers for focused elements document.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { if (document.activeElement.classList.contains('slideshow-thumbnail')) { e.preventDefault(); const index = parseInt(document.activeElement.dataset.index); if (!isNaN(index)) { currentIndex = index; updateSlideshow(); } } } }); }, setupLiveRegion() { // Create an ARIA live region for dynamic announcements const liveRegion = document.createElement('div'); liveRegion.id = 'slideshow-announcer'; liveRegion.setAttribute('aria-live', 'polite'); liveRegion.setAttribute('aria-atomic', 'true'); liveRegion.style.position = 'absolute'; liveRegion.style.width = '1px'; liveRegion.style.height = '1px'; liveRegion.style.overflow = 'hidden'; document.body.appendChild(liveRegion); }, enhanceButtonLabels() { // Add descriptive ARIA labels to buttons const buttons = document.querySelectorAll('.slideshow-button'); buttons.forEach(button => { const action = button.textContent.trim(); let description = ''; switch (action) { case 'Prev': description = 'View previous image'; break; case 'Next': description = 'View next image'; break; case 'Play/Pause': description = 'Toggle slideshow playback'; break; // Add cases for other buttons } if (description) { button.setAttribute('aria-label', description); } }); }, announce(message) { // Make announcement in live region const announcer = document.getElementById('slideshow-announcer'); if (announcer) { announcer.textContent = message; } } }; // -------------------------------- // DEBUG TOOLS AND MONITORING // -------------------------------- const debugTools = { initialize() { // Create debug interface accessible via console window.WyzeSlideshow = { version: '1.9.14', getThumbnails: () => thumbnails, getCurrentState: () => ({ currentIndex, isPlaying, speed, thumbnailCount: thumbnails.length }), forceRefresh: () => { collectThumbnailsAndUpdateUI(); Logger.log(Logger.levels.INFO, 'Forced refresh of thumbnails'); }, clearCache: () => { thumbnails = []; currentIndex = 0; updateSlideshow(); Logger.log(Logger.levels.INFO, 'Cleared thumbnails cache'); }, // Add performance monitoring getPerformanceMetrics: () => this.getPerformanceMetrics(), // Add error tracking getErrorLog: () => this.getErrorLog() }; // Initialize performance monitoring this.initializePerformanceMonitoring(); Logger.log(Logger.levels.INFO, 'Debug tools initialized'); }, initializePerformanceMonitoring() { this.performanceMetrics = { loadTimes: [], errorCount: 0, lastRefresh: Date.now() }; // Monitor thumbnail loading performance const originalPreloadImage = window.preloadImage; window.preloadImage = (src) => { const startTime = performance.now(); originalPreloadImage(src).then(() => { const loadTime = performance.now() - startTime; this.performanceMetrics.loadTimes.push(loadTime); }); }; }, getPerformanceMetrics() { const loadTimes = this.performanceMetrics.loadTimes; return { averageLoadTime: loadTimes.reduce((a, b) => a + b, 0) / loadTimes.length, maxLoadTime: Math.max(...loadTimes), minLoadTime: Math.min(...loadTimes), errorCount: this.performanceMetrics.errorCount, uptime: Date.now() - this.performanceMetrics.lastRefresh }; }, getErrorLog() { return window.wyzeSlideshowLogs.filter(log => log.level === Logger.levels.ERROR || log.level === Logger.levels.WARN ); } }; // -------------------------------- // MEMORY MANAGEMENT SYSTEM // -------------------------------- /* const memoryManager = { initialize() { // Configure memory management settings this.settings = { maxCachedThumbnails: 50, // Maximum number of thumbnails to keep in memory cleanupInterval: 30000, // Run cleanup every 30 seconds preloadLimit: 5, // Number of images to preload ahead/behind maxLogEntries: 1000, // Maximum number of log entries to retain gcThreshold: 100 // Run garbage collection suggestion after this many operations }; // Initialize counters for garbage collection monitoring this.operationCount = 0; // Start periodic cleanup this.startPeriodicCleanup(); Logger.log(Logger.levels.INFO, 'Memory management system initialized'); }, startPeriodicCleanup() { // Set up periodic cleanup of unused resources setInterval(() => { this.cleanupUnusedThumbnails(); this.cleanupUnusedBlobs(); this.pruneLogEntries(); this.checkMemoryUsage(); }, this.settings.cleanupInterval); }, cleanupUnusedThumbnails() { // Remove excess thumbnails while keeping currently visible and adjacent ones if (thumbnails.length > this.settings.maxCachedThumbnails) { // Determine range of thumbnails to keep const keepStart = Math.max(0, currentIndex - this.settings.preloadLimit); const keepEnd = Math.min(thumbnails.length, currentIndex + this.settings.preloadLimit); // Remove thumbnails outside the keep range const removedThumbnails = thumbnails.splice(this.settings.maxCachedThumbnails); Logger.log(Logger.levels.INFO, `Cleaned up ${removedThumbnails.length} excess thumbnails from memory`); // Clean up associated DOM elements this.cleanupThumbnailDOM(); } }, cleanupThumbnailDOM() { // Remove thumbnail elements that are no longer needed const unusedThumbs = document.querySelectorAll('.slideshow-thumbnail:not(.active)'); unusedThumbs.forEach(thumb => { thumb.src = ''; // Clear the source to help with memory thumb.remove(); Logger.log(Logger.levels.DEBUG, 'Removed unused thumbnail from DOM'); }); }, cleanupUnusedBlobs() { // Revoke any outstanding blob URLs that are no longer needed const blobUrls = Array.from(document.querySelectorAll('video')) .map(video => video.src) .filter(src => src.startsWith('blob:')); blobUrls.forEach(url => { if (!document.querySelector(`video[src="${url}"]`)) { URL.revokeObjectURL(url); Logger.log(Logger.levels.DEBUG, `Revoked unused blob URL: ${url}`); } }); }, pruneLogEntries() { // Keep log size manageable by removing oldest entries if (window.wyzeSlideshowLogs?.length > this.settings.maxLogEntries) { const exceeding = window.wyzeSlideshowLogs.length - this.settings.maxLogEntries; window.wyzeSlideshowLogs.splice(0, exceeding); Logger.log(Logger.levels.DEBUG, `Pruned ${exceeding} old log entries`); } }, checkMemoryUsage() { // Monitor operation count and suggest garbage collection if needed this.operationCount++; if (this.operationCount >= this.settings.gcThreshold) { this.operationCount = 0; // Log memory usage statistics if available if (window.performance?.memory) { const memory = window.performance.memory; Logger.log(Logger.levels.INFO, `Memory usage: ${Math.round(memory.usedJSHeapSize / 1048576)}MB ` + `of ${Math.round(memory.jsHeapSizeLimit / 1048576)}MB`); } } } }; */ // -------------------------------- // FINAL INITIALIZATION SYSTEM // -------------------------------- function initializeSlideshowSystem() { // Check if we're already initialized to prevent duplicate setup if (slideshowInitialized) { Logger.log(Logger.levels.WARN, 'Slideshow system already initialized'); return; } try { // Initialize core components in specific order injectStyles(); injectOrientationStyles(); createSlideshowUI(); createHelpOverlay(); addStartButton(); // Initialize feature systems improvedThumbnailCollection(); //memoryManager.initialize(); // touchControls.initialize(); timelineNavigation.initialize(); enhancedKeyboardControls.initialize(); accessibilityEnhancements.initialize(); debugTools.initialize(); // Set up DOM observation for dynamic content observeDOMChanges(); // Perform initial thumbnail collection collectThumbnailsAndUpdateUI(); slideshowInitialized = true; Logger.log(Logger.levels.INFO, 'Slideshow system fully initialized'); } catch (error) { Logger.log(Logger.levels.ERROR, 'Error during slideshow initialization', error); // Attempt to cleanup if initialization fails performEmergencyCleanup(); } } function performEmergencyCleanup() { try { // Define the elements array with IDs of elements to remove const elements = [ 'slideshow-container', 'start-slideshow-btn', 'slideshow-help-overlay', 'timeline-preview', 'loading-spinner', 'slideshow-announcer' ]; // Remove DOM elements elements.forEach(id => { const element = document.getElementById(id); if (element) { element.remove(); } }); // Rest of the function remains the same... } catch (error) { Logger.log(Logger.levels.ERROR, 'Error during emergency cleanup', error); } } // Initialize when page is ready if (document.readyState === 'loading') { window.addEventListener('load', initializeSlideshowSystem); } else { initializeSlideshowSystem(); } })(); // End of IIFE