initial
This commit is contained in:
commit
b7d788be14
45
Readme.md
Normal file
45
Readme.md
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
450
src/index.html
Normal file
450
src/index.html
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user