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