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