Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Advanced Image Slideshow Viewer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <style> | |
| body { | |
| background-color: #1a1a2e; | |
| color: #e6e6e6; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| .image-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .image-display { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| transition: opacity 0.3s ease; | |
| } | |
| .image-display.fade-out { | |
| opacity: 0; | |
| } | |
| .banner { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| background: linear-gradient(135deg, #2d1b69 0%, #1a1a2e 50%, #16213e 100%); | |
| padding: 15px 0; | |
| z-index: 20; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.5); | |
| border-bottom: 2px solid #6a6aaa; | |
| transition: transform 0.3s ease; | |
| text-align: center; | |
| } | |
| .banner.hidden { | |
| transform: translateY(-100%); | |
| } | |
| .banner-title { | |
| font-size: 28px; | |
| font-weight: bold; | |
| background: linear-gradient(45deg, #ffffff, #e6e6e6, #ccccff); | |
| background-clip: text; | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-shadow: 0 2px 10px rgba(255,255,255,0.1); | |
| letter-spacing: 1px; | |
| } | |
| .controls { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(0,0,0,0.9); | |
| padding: 15px 25px; | |
| border-radius: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| align-items: center; | |
| z-index: 10; | |
| transition: opacity 0.3s ease; | |
| box-shadow: 0 0 20px rgba(0,0,0,0.5); | |
| } | |
| .control-row { | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| .toggle-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 5px 10px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 10px; | |
| } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 5px 10px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 10px; | |
| } | |
| .toggle { | |
| position: relative; | |
| display: inline-block; | |
| width: 50px; | |
| height: 24px; | |
| } | |
| .toggle input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: #3a3a5a; | |
| transition: .4s; | |
| border-radius: 24px; | |
| } | |
| .slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 16px; | |
| width: 16px; | |
| left: 4px; | |
| bottom: 4px; | |
| background-color: white; | |
| transition: .4s; | |
| border-radius: 50%; | |
| } | |
| input:checked + .slider { | |
| background-color: #6a6aaa; | |
| } | |
| input:not(:checked) + .slider { | |
| background-color: #3a3a5a; | |
| opacity: 0.7; | |
| } | |
| input:checked + .slider:before { | |
| transform: translateX(26px); | |
| } | |
| .range-slider { | |
| width: 120px; | |
| margin: 0 10px; | |
| } | |
| .range-value { | |
| min-width: 30px; | |
| text-align: center; | |
| font-weight: bold; | |
| color: #fff; | |
| } | |
| .controls.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .file-input { | |
| display: none; | |
| } | |
| .btn { | |
| background: #4a4a8a; | |
| color: white; | |
| border: none; | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
| } | |
| .btn:hover { | |
| background: #6a6aaa; | |
| } | |
| .reload-btn { | |
| position: absolute; | |
| top: 80px; | |
| right: 20px; | |
| z-index: 10; | |
| display: none; | |
| } | |
| .status { | |
| font-size: 14px; | |
| color: #fff; | |
| background: rgba(0,0,0,0.5); | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| margin-left: 10px; | |
| max-width: 300px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .fullscreen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 5; | |
| } | |
| .loading { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| color: #6a6aaa; | |
| font-size: 18px; | |
| z-index: 8; | |
| } | |
| </style> | |
| </head> | |
| <body class="font-sans"> | |
| <div class="banner" id="banner"> | |
| <h1 class="banner-title">Advanced Image Slideshow Viewer</h1> | |
| </div> | |
| <div class="image-container"> | |
| <img id="imageDisplay" class="image-display" alt="Slideshow Image" style="display: none;"> | |
| <div class="loading" id="loading" style="display: none;">Loading...</div> | |
| <div class="fullscreen" id="clickArea"></div> | |
| <button id="reloadBtn" class="btn reload-btn"> | |
| <i data-feather="refresh-cw"></i> | |
| Reload | |
| </button> | |
| <div class="controls" id="controls"> | |
| <div class="control-row"> | |
| <button id="selectFilesBtn" class="btn"> | |
| <i data-feather="image"></i> | |
| Select Images | |
| </button> | |
| <button id="startBtn" class="btn" style="display: none;"> | |
| <i data-feather="play"></i> | |
| Start Slideshow | |
| </button> | |
| <span id="status" class="status">No images selected</span> | |
| <input type="file" id="fileInput" class="file-input" accept="image/*" multiple> | |
| </div> | |
| <div class="control-row"> | |
| <div class="toggle-container"> | |
| <label class="toggle"> | |
| <input type="checkbox" id="autoplayToggle" checked> | |
| <span class="slider"></span> | |
| </label> | |
| <span>Autoplay</span> | |
| </div> | |
| <div class="slider-container"> | |
| <span>Duration:</span> | |
| <input type="range" id="durationSlider" class="range-slider" min="1" max="10" value="3" step="1"> | |
| <span id="durationValue" class="range-value">3s</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| feather.replace(); | |
| const fileInput = document.getElementById('fileInput'); | |
| const selectFilesBtn = document.getElementById('selectFilesBtn'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const imageDisplay = document.getElementById('imageDisplay'); | |
| const status = document.getElementById('status'); | |
| const clickArea = document.getElementById('clickArea'); | |
| const autoplayToggle = document.getElementById('autoplayToggle'); | |
| const durationSlider = document.getElementById('durationSlider'); | |
| const durationValue = document.getElementById('durationValue'); | |
| const controls = document.getElementById('controls'); | |
| const banner = document.getElementById('banner'); | |
| const loading = document.getElementById('loading'); | |
| let files = []; | |
| let fileNames = []; | |
| let currentFileIndex = 0; | |
| let duration = parseInt(durationSlider.value) * 1000; // Convert to milliseconds | |
| let isAutoplay = autoplayToggle.checked; | |
| let slideshowTimer = null; | |
| let isPlaying = false; | |
| // Update duration value display | |
| durationSlider.addEventListener('input', (e) => { | |
| duration = parseInt(e.target.value) * 1000; | |
| durationValue.textContent = parseInt(e.target.value) + 's'; | |
| if (isPlaying && isAutoplay) { | |
| // Restart timer with new duration | |
| clearTimeout(slideshowTimer); | |
| startSlideshowTimer(); | |
| } | |
| }); | |
| // Handle autoplay toggle | |
| autoplayToggle.addEventListener('change', () => { | |
| isAutoplay = autoplayToggle.checked; | |
| console.log('Autoplay toggled:', isAutoplay); | |
| if (isAutoplay && isPlaying) { | |
| startSlideshowTimer(); | |
| } else { | |
| clearTimeout(slideshowTimer); | |
| } | |
| if (files.length > 0) { | |
| updateStatus(); | |
| } | |
| }); | |
| function updateStatus() { | |
| if (fileNames.length > 0) { | |
| const fileName = fileNames[currentFileIndex]; | |
| status.textContent = `Showing ${currentFileIndex + 1}/${fileNames.length}: ${fileName}`; | |
| } | |
| } | |
| function startSlideshowTimer() { | |
| if (!isAutoplay) return; | |
| clearTimeout(slideshowTimer); | |
| slideshowTimer = setTimeout(() => { | |
| handleImageTimeout(); | |
| }, duration); | |
| } | |
| function handleImageTimeout() { | |
| console.log('Image timeout. Autoplay:', isAutoplay); | |
| if (isAutoplay) { | |
| // Move to next image | |
| console.log('Moving to next image'); | |
| currentFileIndex = (currentFileIndex + 1) % fileNames.length; | |
| showImage(currentFileIndex); | |
| } else { | |
| // When autoplay is off, just keep showing current image | |
| updateStatus(); | |
| } | |
| } | |
| selectFilesBtn.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| startBtn.addEventListener('click', () => { | |
| if (files.length > 0) { | |
| currentFileIndex = 0; | |
| showImage(currentFileIndex); | |
| } | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| const selectedFiles = Array.from(e.target.files); | |
| if (selectedFiles.length > 0) { | |
| fileNames = selectedFiles.map(file => file.name); | |
| files = selectedFiles; | |
| status.textContent = `${selectedFiles.length} images selected - Ready to view`; | |
| startBtn.style.display = 'flex'; // Show start button | |
| currentFileIndex = 0; | |
| isPlaying = false; | |
| clearTimeout(slideshowTimer); | |
| // Don't auto-start, wait for user to click start button | |
| console.log('Files selected, waiting for start button click'); | |
| } | |
| }); | |
| let longPressTimer; | |
| clickArea.addEventListener('mousedown', () => { | |
| longPressTimer = setTimeout(() => { | |
| controls.classList.toggle('hidden'); | |
| banner.classList.toggle('hidden'); | |
| }, 1000); | |
| }); | |
| clickArea.addEventListener('mouseup', () => { | |
| clearTimeout(longPressTimer); | |
| }); | |
| clickArea.addEventListener('mouseleave', () => { | |
| clearTimeout(longPressTimer); | |
| }); | |
| clickArea.addEventListener('click', (e) => { | |
| if (e.target === clickArea && fileNames.length > 0) { | |
| currentFileIndex = (currentFileIndex + 1) % fileNames.length; | |
| showImage(currentFileIndex); | |
| } | |
| }); | |
| function showImage(index) { | |
| if (index >= 0 && index < files.length) { | |
| console.log('Showing image:', index, 'of', files.length); | |
| clearTimeout(slideshowTimer); | |
| // Clean up previous URLs | |
| if (imageDisplay.src && imageDisplay.src.startsWith('blob:')) { | |
| URL.revokeObjectURL(imageDisplay.src); | |
| } | |
| isPlaying = true; | |
| // Show loading | |
| loading.style.display = 'block'; | |
| imageDisplay.style.display = 'none'; | |
| // Load current image | |
| const currentFile = files[index]; | |
| const currentImageURL = URL.createObjectURL(currentFile); | |
| // Create new image to preload | |
| const tempImage = new Image(); | |
| tempImage.onload = () => { | |
| imageDisplay.src = currentImageURL; | |
| imageDisplay.style.display = 'block'; | |
| loading.style.display = 'none'; | |
| updateStatus(); | |
| // Only hide controls and banner if slideshow is actually starting | |
| if (isPlaying) { | |
| controls.classList.add('hidden'); | |
| banner.classList.add('hidden'); | |
| } | |
| // Only request fullscreen if slideshow is running | |
| if (isPlaying && !document.fullscreenElement && document.body.requestFullscreen) { | |
| document.body.requestFullscreen().catch(e => { | |
| console.log('Fullscreen error:', e); | |
| }); | |
| } | |
| // Start timer for autoplay | |
| if (isAutoplay) { | |
| startSlideshowTimer(); | |
| } | |
| }; | |
| tempImage.onerror = () => { | |
| loading.style.display = 'none'; | |
| status.textContent = `Error loading image: ${fileNames[index]}`; | |
| console.error('Error loading image:', fileNames[index]); | |
| }; | |
| tempImage.src = currentImageURL; | |
| } | |
| } | |
| // Handle fullscreen toggle | |
| clickArea.addEventListener('dblclick', () => { | |
| if (!document.fullscreenElement) { | |
| if (document.body.requestFullscreen) { | |
| document.body.requestFullscreen(); | |
| } else if (document.body.webkitRequestFullscreen) { | |
| document.body.webkitRequestFullscreen(); | |
| } else if (document.body.msRequestFullscreen) { | |
| document.body.msRequestFullscreen(); | |
| } | |
| } else { | |
| if (document.exitFullscreen) { | |
| document.exitFullscreen(); | |
| } else if (document.webkitExitFullscreen) { | |
| document.webkitExitFullscreen(); | |
| } else if (document.msExitFullscreen) { | |
| document.msExitFullscreen(); | |
| } | |
| } | |
| }); | |
| // Keyboard navigation | |
| document.addEventListener('keydown', (e) => { | |
| if (files.length === 0) return; | |
| switch(e.key) { | |
| case 'ArrowRight': | |
| case ' ': | |
| e.preventDefault(); | |
| currentFileIndex = (currentFileIndex + 1) % fileNames.length; | |
| repeatCount = 0; | |
| showImage(currentFileIndex); | |
| break; | |
| case 'ArrowLeft': | |
| e.preventDefault(); | |
| currentFileIndex = (currentFileIndex - 1 + fileNames.length) % fileNames.length; | |
| repeatCount = 0; | |
| showImage(currentFileIndex); | |
| break; | |
| case 'Escape': | |
| controls.classList.remove('hidden'); | |
| banner.classList.remove('hidden'); | |
| break; | |
| } | |
| }); | |
| // Reload button functionality | |
| const reloadBtn = document.getElementById('reloadBtn'); | |
| reloadBtn.addEventListener('click', () => { | |
| location.reload(); | |
| }); | |
| // Show reload button when not in fullscreen | |
| document.addEventListener('fullscreenchange', () => { | |
| reloadBtn.style.display = document.fullscreenElement ? 'none' : 'block'; | |
| }); | |
| // Cleanup on page unload | |
| window.addEventListener('beforeunload', () => { | |
| clearTimeout(slideshowTimer); | |
| if (imageDisplay.src && imageDisplay.src.startsWith('blob:')) { | |
| URL.revokeObjectURL(imageDisplay.src); | |
| } | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |