ScreenshotPlayer/src/index.html
2025-05-27 17:11:36 +01:00

624 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Screenshot Timeline Player</title>
<link href="https://cdn.jsdelivr.net/npm/beercss@3.11.11/dist/cdn/beer.min.css" rel="stylesheet">
<style>
body {
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.main-container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 2rem;
color: white;
}
.controls-card {
margin-bottom: 1rem;
}
.thumbnails-container {
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
border: 1px solid rgba(255,255,255,0.2);
max-height: 150px;
overflow-y: auto;
}
.thumbnails-scroll {
display: flex;
gap: 0.5rem;
padding: 0.5rem 0;
overflow-x: auto;
scroll-behavior: smooth;
}
.thumbnail {
min-width: 100px;
height: 80px;
border-radius: 0.25rem;
cursor: pointer;
object-fit: cover;
border: 2px solid transparent;
transition: all 0.3s ease;
opacity: 0.7;
}
.thumbnail:hover {
opacity: 1;
transform: scale(1.05);
}
.thumbnail.active {
border-color: #ff6b6b;
opacity: 1;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
}
.thumbnail.selected {
border-color: #4caf50;
background-color: rgba(76, 175, 80, 0.3);
}
.image-viewer {
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
border-radius: 0.5rem;
padding: 1rem;
border: 1px solid rgba(255,255,255,0.2);
min-height: 500px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.main-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 0.5rem;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.image-info {
position: absolute;
top: 1rem;
left: 1rem;
background: rgba(0,0,0,0.8);
color: white;
padding: 0.5rem 1rem;
border-radius: 1rem;
font-size: 0.875rem;
}
.timeline-progress {
height: 4px;
background: linear-gradient(90deg, #ff6b6b, #ee5a24);
border-radius: 2px;
transition: width 0.3s ease;
width: 0%;
margin: 0 0 1rem;
}
.timeline-labels {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
color: rgba(255,255,255,0.8);
margin-bottom: 0.5rem;
height: 1.2rem;
}
.control-group {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.speed-control {
display: flex;
align-items: center;
gap: 0.5rem;
color: white;
}
.placeholder {
text-align: center;
color: rgba(255,255,255,0.7);
font-size: 1.2rem;
}
.file-input {
display: none;
}
.status {
color: rgba(255,255,255,0.9);
margin-left: auto;
}
@media (max-width: 768px) {
.control-group {
flex-direction: column;
align-items: stretch;
}
.status {
margin-left: 0;
text-align: center;
}
}
</style>
</head>
<body class="dark">
<div class="main-container">
<div class="header">
<h1>📸 Screenshot Timeline</h1>
<p>Play your screenshots in chronological order</p>
</div>
<article class="image-viewer" id="imageContainer">
<div class="placeholder">
Select a folder containing screenshots to begin
</div>
</article>
<div class="thumbnails-container" id="thumbnailsContainer" style="display: none;">
<div class="thumbnails-scroll" id="thumbnailsScroll">
<!-- Thumbnails will be added here -->
</div>
</div>
<article class="controls-card">
<div class="status" id="status">No folder selected</div>
<div class="timeline-progress" id="timelineProgress"></div>
<div class="timeline-labels" id="timelineLabels" style="display: flex; justify-content: space-between; font-size: 0.8rem; color: #fff; margin-bottom: 0.5rem;"></div>
<div class="control-group">
<button class="button primary" onclick="selectFolder()">
<i>folder</i>
<span>Select Folder</span>
</button>
<button class="button primary" onclick="goToFirst()" disabled id="firstBtn" class="round-btn" title="First">
<i>first_page</i>
</button>
<button class="button primary" onclick="previousImage()" disabled id="prevBtn" class="round-btn" title="Previous">
<i>skip_previous</i>
</button>
<button class="button primary" id="playBtn" onclick="togglePlay()" disabled>
<i>play_arrow</i>
<span>Play</span>
</button>
<button class="button primary" onclick="nextImage()" disabled id="nextBtn" class="round-btn" title="Next">
<i>skip_next</i>
</button>
<button class="button primary" onclick="goToLast()" disabled id="lastBtn" class="round-btn" title="Last">
<i>last_page</i>
</button>
<div class="speed-control">
<span>Speed:</span>
<input type="range" id="speedSlider" min="0.5" max="5" step="0.5" value="1" onchange="updateSpeed()">
<span id="speedValue">1x</span>
</div>
<button class="button danger" id="deleteBtn" onclick="deleteSelectedImages()" disabled>
<i>delete</i>
</button>
</div>
</article>
</div>
<input type="file" id="folderInput" class="file-input" webkitdirectory>
<script>
let images = [];
let currentIndex = 0;
let isPlaying = false;
let playInterval = null;
let playSpeed = 1000;
let selectedIndices = [];
function selectFolder() {
document.getElementById('folderInput').click();
}
document.getElementById('folderInput').addEventListener('change', async function(e) {
const files = Array.from(e.target.files);
await loadImages(files);
});
async function loadImages(files) {
const imageFiles = files.filter(file =>
file.type.startsWith('image/') &&
/\.(png|jpg|jpeg|gif|bmp|webp)$/i.test(file.name)
);
if (imageFiles.length === 0) {
updateStatus('No image files found');
return;
}
updateStatus(`Loading ${imageFiles.length} images...`);
const imageData = await Promise.all(
imageFiles.map(async file => ({
file: file,
url: URL.createObjectURL(file),
name: file.name,
createdDate: new Date(file.lastModified),
size: file.size
}))
);
images = imageData.sort((a, b) => a.createdDate - b.createdDate);
currentIndex = 0;
createThumbnails();
updateButtons();
displayCurrentImage();
updateStatus(`Loaded ${images.length} images`);
}
function createThumbnails() {
const container = document.getElementById('thumbnailsScroll');
const thumbnailsContainer = document.getElementById('thumbnailsContainer');
container.innerHTML = '';
if (images.length === 0) {
thumbnailsContainer.style.display = 'none';
return;
}
thumbnailsContainer.style.display = 'block';
images.forEach((img, index) => {
const thumb = document.createElement('img');
thumb.src = img.url;
thumb.className = 'thumbnail';
thumb.onclick = (e) => handleThumbnailClick(e, index);
thumb.title = `${img.name} - ${img.createdDate.toLocaleString()}`;
container.appendChild(thumb);
});
updateThumbnailHighlight();
}
function handleThumbnailClick(e, index) {
if (e.ctrlKey || e.metaKey) {
// Toggle selection
if (selectedIndices.includes(index)) {
selectedIndices = selectedIndices.filter(i => i !== index);
} else {
selectedIndices.push(index);
}
} else if (e.shiftKey && selectedIndices.length > 0) {
// Range select
const last = selectedIndices[selectedIndices.length - 1];
const [start, end] = [Math.min(last, index), Math.max(last, index)];
selectedIndices = [];
for (let i = start; i <= end; i++) selectedIndices.push(i);
} else {
// Single select
selectedIndices = [index];
}
updateThumbnailHighlight();
if (selectedIndices.length === 1) {
currentIndex = selectedIndices[0];
displayCurrentImage();
} else {
displayCurrentImage();
}
updateDeleteButton();
}
function updateThumbnailHighlight() {
const thumbnails = document.querySelectorAll('.thumbnail');
thumbnails.forEach((thumb, index) => {
thumb.classList.toggle('active', index === currentIndex && selectedIndices.length <= 1);
thumb.classList.toggle('selected', selectedIndices.includes(index));
});
// Scroll active thumbnail into view
if (selectedIndices.length === 1 && thumbnails[selectedIndices[0]]) {
thumbnails[selectedIndices[0]].scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
}
function goToFirst() {
if (images.length > 0) {
currentIndex = 0;
displayCurrentImage();
}
}
function goToLast() {
if (images.length > 0) {
currentIndex = images.length - 1;
displayCurrentImage();
}
}
function goToImage(index) {
if (index >= 0 && index < images.length) {
currentIndex = index;
displayCurrentImage();
updateThumbnailHighlight();
}
}
function displayCurrentImage() {
const container = document.getElementById('imageContainer');
if (images.length === 0) {
container.innerHTML = '<div class="placeholder">No images loaded</div>';
return;
}
if (selectedIndices.length > 1) {
container.innerHTML = `<div class="placeholder">${selectedIndices.length} images selected. Use the Delete Selected button to remove them.</div>`;
return;
}
const current = images[currentIndex];
const createdDate = current.createdDate.toLocaleString();
container.innerHTML = `
<img src="${current.url}" alt="${current.name}" class="main-image">
<div class="image-info">
${currentIndex + 1} / ${images.length}<br>
${current.name}<br>
${createdDate}
</div>
`;
updateTimeline();
updateThumbnailHighlight();
}
function updateTimeline() {
const progress = images.length > 0 ? ((currentIndex + 1) / images.length) * 100 : 0;
document.getElementById('timelineProgress').style.width = progress + '%';
updateTimelineLabels();
}
function updateTimelineLabels() {
const labelsContainer = document.getElementById('timelineLabels');
labelsContainer.innerHTML = '';
if (images.length === 0) return;
const firstDate = images[0].createdDate;
const lastDate = images[images.length - 1].createdDate;
const totalDuration = lastDate.getTime() - firstDate.getTime();
// Aim for approximately 6-8 labels depending on the container width
const desiredLabels = Math.min(8, Math.max(6, Math.floor(window.innerWidth / 200)));
const labels = [];
// Always add the first timestamp
labels.push({
time: firstDate,
pos: 0
});
// Add evenly spaced timestamps
for (let i = 1; i < desiredLabels - 1; i++) {
const time = new Date(firstDate.getTime() + (totalDuration * i / (desiredLabels - 1)));
// Find closest image to this time
let closestIdx = 0;
let minDiff = Math.abs(images[0].createdDate - time);
for (let j = 1; j < images.length; j++) {
const diff = Math.abs(images[j].createdDate - time);
if (diff < minDiff) {
minDiff = diff;
closestIdx = j;
}
}
const pos = (closestIdx / (images.length - 1)) * 100;
labels.push({
time: images[closestIdx].createdDate,
pos: pos
});
}
// Add the last timestamp
labels.push({
time: lastDate,
pos: 100
});
// Remove any labels that are too close to each other
const filteredLabels = labels.filter((label, index) => {
if (index === 0) return true;
return Math.abs(label.pos - labels[index - 1].pos) >= 5;
});
// Calculate if we need to show seconds (if total duration is less than 5 minutes)
const showSeconds = totalDuration < 5 * 60 * 1000;
// Calculate optimal spacing between labels
const containerWidth = labelsContainer.offsetWidth;
const labelWidth = 70; // Approximate width of a time label
const minSpacing = labelWidth * 1.5; // Minimum space needed between labels
const maxLabels = Math.floor(containerWidth / minSpacing);
const targetLabels = Math.min(filteredLabels.length, maxLabels);
// Select evenly spaced labels
const finalLabels = [];
if (filteredLabels.length > 0) {
const step = (filteredLabels.length - 1) / (targetLabels - 1);
for (let i = 0; i < targetLabels; i++) {
const index = Math.round(i * step);
finalLabels.push(filteredLabels[index]);
}
}
// Round the timestamps
const roundedLabels = finalLabels.map(label => {
const newTime = new Date(label.time);
if (showSeconds) {
// Round seconds to nearest 5
const seconds = newTime.getSeconds();
const roundedSeconds = Math.round(seconds / 5) * 5;
newTime.setSeconds(roundedSeconds);
} else {
// Round minutes to nearest 5 and set seconds to 0
const minutes = newTime.getMinutes();
const roundedMinutes = Math.round(minutes / 5) * 5;
newTime.setMinutes(roundedMinutes);
newTime.setSeconds(0);
}
return {
...label,
time: newTime
};
});
// Render labels
labelsContainer.style.position = 'relative';
labelsContainer.innerHTML = roundedLabels.map(l => {
const options = showSeconds
? {hour: '2-digit', minute:'2-digit', second:'2-digit'}
: {hour: '2-digit', minute:'2-digit'};
return `<span style="position: absolute; left: ${l.pos}%; transform: translateX(-50%); white-space: nowrap;">
${l.time.toLocaleTimeString([], options)}
</span>`;
}).join('');
}
function togglePlay() {
if (isPlaying) {
stopPlay();
} else {
startPlay();
}
}
function startPlay() {
if (images.length === 0) return;
isPlaying = true;
const playBtn = document.getElementById('playBtn');
playBtn.innerHTML = '<i>pause</i><span>Pause</span>';
playInterval = setInterval(() => {
if (currentIndex < images.length - 1) {
nextImage();
} else {
stopPlay();
}
}, playSpeed);
}
function stopPlay() {
isPlaying = false;
const playBtn = document.getElementById('playBtn');
playBtn.innerHTML = '<i>play_arrow</i><span>Play</span>';
if (playInterval) {
clearInterval(playInterval);
playInterval = null;
}
}
function nextImage() {
if (currentIndex < images.length - 1) {
currentIndex++;
displayCurrentImage();
}
}
function previousImage() {
if (currentIndex > 0) {
currentIndex--;
displayCurrentImage();
}
}
function updateSpeed() {
const speed = parseFloat(document.getElementById('speedSlider').value);
playSpeed = 1000 / speed;
document.getElementById('speedValue').textContent = speed + 'x';
if (isPlaying) {
clearInterval(playInterval);
playInterval = setInterval(() => {
if (currentIndex < images.length - 1) {
nextImage();
} else {
stopPlay();
}
}, playSpeed);
}
}
function updateButtons() {
const hasImages = images.length > 0;
document.getElementById('playBtn').disabled = !hasImages;
document.getElementById('firstBtn').disabled = !hasImages;
document.getElementById('prevBtn').disabled = !hasImages;
document.getElementById('nextBtn').disabled = !hasImages;
document.getElementById('lastBtn').disabled = !hasImages;
}
function updateStatus(message) {
document.getElementById('status').textContent = message;
}
function updateDeleteButton() {
document.getElementById('deleteBtn').disabled = selectedIndices.length === 0;
}
function deleteSelectedImages() {
if (selectedIndices.length === 0) return;
// Remove selected images
images = images.filter((_, idx) => !selectedIndices.includes(idx));
// Reset selection and currentIndex
selectedIndices = [];
currentIndex = Math.min(currentIndex, images.length - 1);
createThumbnails();
updateButtons();
displayCurrentImage();
updateStatus(`Deleted selected images.`);
updateDeleteButton();
}
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
switch(e.code) {
case 'Space':
e.preventDefault();
if (images.length > 0) togglePlay();
break;
case 'ArrowLeft':
previousImage();
break;
case 'ArrowRight':
nextImage();
break;
}
});
</script>
</body>
</html>