This commit is contained in:
Jose Aquino Jr 2025-05-27 15:57:47 +01:00
commit b7d788be14
3 changed files with 495 additions and 0 deletions

45
Readme.md Normal file
View File

@ -0,0 +1,45 @@
# Screenshot Timeline Player
Screenshot Timeline Player is a web app that lets you view and play back a sequence of screenshots (or other images) in chronological order, like a timeline slideshow.
![Screenshot Timeline Player UI](screenshot.png)
## Features
- **Folder Selection:** Load a folder of images (screenshots) at once.
- **Timeline Playback:** Play images in order, like a video.
- **Playback Controls:** Play, pause, go to first/last, next/previous image.
- **Speed Control:** Adjust playback speed (0.5x to 5x).
- **Thumbnails:** See and jump to any image using the thumbnail strip.
- **Keyboard Shortcuts:**
- `Space`: Play/Pause
- `←` / `→`: Previous/Next image
## Usage
1. **Open the app** in your browser (open `src/index.html`).
2. Click **Select Folder** and choose a folder containing your screenshots or images.
3. Use the playback controls or keyboard shortcuts to navigate through your images.
## Supported Image Formats
- PNG, JPG, JPEG, GIF, BMP, WEBP
## Requirements
- A modern web browser (Chrome, Edge, Firefox, Safari).
- No installation or backend required.
## How it Works
- The app uses the browser's [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API) to load images from your local folder.
- Images are sorted by their last modified date to create a timeline.
## License
This project is provided as-is for personal use.
---
Made with [BeerCSS](https://www.beercss.com/) 🍺

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

450
src/index.html Normal file
View File

@ -0,0 +1,450 @@
<!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);
}
.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.5rem 0;
}
.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="control-group">
<button class="button primary" onclick="selectFolder()">
<i>folder</i>
<span>Select Folder</span>
</button>
<button class="button primary" id="playBtn" onclick="togglePlay()" disabled>
<i>play_arrow</i>
<span>Play</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" 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>
<div class="status" id="status">No folder selected</div>
</div>
<div class="timeline-progress" id="timelineProgress"></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;
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 = () => goToImage(index);
thumb.title = `${img.name} - ${img.createdDate.toLocaleString()}`;
container.appendChild(thumb);
});
updateThumbnailHighlight();
}
function updateThumbnailHighlight() {
const thumbnails = document.querySelectorAll('.thumbnail');
thumbnails.forEach((thumb, index) => {
thumb.classList.toggle('active', index === currentIndex);
});
// Scroll active thumbnail into view
// if (thumbnails[currentIndex]) {
// thumbnails[currentIndex].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;
}
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 + '%';
}
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;
}
// 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>