// ==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