image-slideshow / index.html
MarkTheArtist's picture
Remove repeat interface element and set default to 1 - Initial Deployment
fd901fd verified
<!DOCTYPE html>
<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>