Created
March 5, 2026 05:54
-
-
Save perryism/c5704d7090b7045c9f752cd293fe358c to your computer and use it in GitHub Desktop.
video player reel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Local Video Player</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| } | |
| .app-wrapper { | |
| display: flex; | |
| gap: 20px; | |
| max-width: 1400px; | |
| width: 100%; | |
| height: 90vh; | |
| } | |
| .container { | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| padding: 40px; | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| min-width: 400px; | |
| position: relative; | |
| } | |
| .resize-handle { | |
| position: absolute; | |
| background: transparent; | |
| z-index: 10; | |
| } | |
| .resize-handle-right { | |
| right: 0; | |
| top: 0; | |
| bottom: 0; | |
| width: 10px; | |
| cursor: ew-resize; | |
| } | |
| .resize-handle-right:hover { | |
| background: rgba(102, 126, 234, 0.2); | |
| } | |
| .resize-handle-right::after { | |
| content: ''; | |
| position: absolute; | |
| right: 2px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| width: 3px; | |
| height: 40px; | |
| background: #667eea; | |
| border-radius: 2px; | |
| opacity: 0; | |
| transition: opacity 0.2s ease; | |
| } | |
| .resize-handle-right:hover::after { | |
| opacity: 0.6; | |
| } | |
| .container.resizing { | |
| user-select: none; | |
| } | |
| .reel-sidebar { | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| padding: 20px; | |
| width: 300px; | |
| display: flex; | |
| flex-direction: column; | |
| transition: all 0.3s ease; | |
| } | |
| .reel-sidebar.dragover { | |
| background: #f8f9ff; | |
| box-shadow: 0 20px 60px rgba(102, 126, 234, 0.4); | |
| transform: scale(1.02); | |
| } | |
| .reel-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| padding-bottom: 15px; | |
| border-bottom: 2px solid #f0f0f0; | |
| } | |
| .reel-title { | |
| font-size: 1.2em; | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| .reel-count { | |
| background: #667eea; | |
| color: white; | |
| padding: 4px 12px; | |
| border-radius: 12px; | |
| font-size: 0.85em; | |
| font-weight: 500; | |
| } | |
| .reel-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .reel-list::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .reel-list::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| border-radius: 10px; | |
| } | |
| .reel-list::-webkit-scrollbar-thumb { | |
| background: #667eea; | |
| border-radius: 10px; | |
| } | |
| .reel-item { | |
| background: #f8f9ff; | |
| border-radius: 10px; | |
| padding: 12px; | |
| cursor: move; | |
| transition: all 0.3s ease; | |
| border: 2px solid transparent; | |
| position: relative; | |
| user-select: none; | |
| } | |
| .reel-item:hover { | |
| background: #e8ebff; | |
| transform: translateX(-5px); | |
| } | |
| .reel-item.active { | |
| border-color: #667eea; | |
| background: #e8ebff; | |
| } | |
| .reel-item.dragging { | |
| opacity: 0.5; | |
| cursor: grabbing; | |
| } | |
| .reel-item.drag-over { | |
| border-top: 3px solid #667eea; | |
| } | |
| .reel-item-name { | |
| font-size: 0.9em; | |
| color: #333; | |
| font-weight: 500; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| margin-bottom: 5px; | |
| } | |
| .reel-item-duration { | |
| font-size: 0.75em; | |
| color: #999; | |
| } | |
| .reel-item-remove { | |
| position: absolute; | |
| top: 8px; | |
| right: 8px; | |
| background: #ff4757; | |
| color: white; | |
| border: none; | |
| border-radius: 50%; | |
| width: 20px; | |
| height: 20px; | |
| font-size: 0.8em; | |
| cursor: pointer; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| } | |
| .reel-item:hover .reel-item-remove { | |
| display: flex; | |
| } | |
| .reel-item-remove:hover { | |
| background: #d63447; | |
| transform: scale(1.1); | |
| } | |
| .reel-controls { | |
| padding-top: 15px; | |
| border-top: 2px solid #f0f0f0; | |
| } | |
| .loop-toggle { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px; | |
| background: #f8f9ff; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: background 0.3s ease; | |
| } | |
| .loop-toggle:hover { | |
| background: #e8ebff; | |
| } | |
| .loop-toggle input[type="checkbox"] { | |
| width: 18px; | |
| height: 18px; | |
| cursor: pointer; | |
| } | |
| .loop-toggle label { | |
| cursor: pointer; | |
| color: #333; | |
| font-size: 0.9em; | |
| font-weight: 500; | |
| } | |
| .clear-reel-btn { | |
| margin-top: 10px; | |
| background: #ff4757; | |
| color: white; | |
| border: none; | |
| padding: 10px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 0.9em; | |
| font-weight: 500; | |
| transition: background 0.3s ease; | |
| } | |
| .clear-reel-btn:hover { | |
| background: #d63447; | |
| } | |
| .ffmpeg-section { | |
| margin-top: 15px; | |
| padding-top: 15px; | |
| border-top: 2px solid #f0f0f0; | |
| } | |
| .ffmpeg-title { | |
| font-size: 0.95em; | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| .ffmpeg-command { | |
| background: #2d2d2d; | |
| color: #f8f8f2; | |
| padding: 12px; | |
| border-radius: 8px; | |
| font-family: 'Monaco', 'Menlo', 'Courier New', monospace; | |
| font-size: 0.75em; | |
| overflow-x: auto; | |
| margin-bottom: 10px; | |
| position: relative; | |
| } | |
| .ffmpeg-command code { | |
| display: block; | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| } | |
| .copy-btn { | |
| position: absolute; | |
| top: 8px; | |
| right: 8px; | |
| background: #667eea; | |
| color: white; | |
| border: none; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 0.85em; | |
| transition: background 0.2s ease; | |
| } | |
| .copy-btn:hover { | |
| background: #764ba2; | |
| } | |
| .copy-btn.copied { | |
| background: #2ecc71; | |
| } | |
| .mylist-section { | |
| margin-top: 10px; | |
| } | |
| .mylist-title { | |
| font-size: 0.85em; | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 8px; | |
| } | |
| .mylist-content { | |
| background: #2d2d2d; | |
| color: #f8f8f2; | |
| padding: 12px; | |
| border-radius: 8px; | |
| font-family: 'Monaco', 'Menlo', 'Courier New', monospace; | |
| font-size: 0.75em; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| position: relative; | |
| } | |
| .mylist-content::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .mylist-content::-webkit-scrollbar-track { | |
| background: #1a1a1a; | |
| border-radius: 10px; | |
| } | |
| .mylist-content::-webkit-scrollbar-thumb { | |
| background: #667eea; | |
| border-radius: 10px; | |
| } | |
| .mylist-content code { | |
| display: block; | |
| white-space: pre; | |
| color: #a6e22e; | |
| } | |
| .ffmpeg-empty { | |
| text-align: center; | |
| color: #999; | |
| padding: 20px; | |
| font-size: 0.85em; | |
| font-style: italic; | |
| } | |
| .reel-empty { | |
| text-align: center; | |
| color: #999; | |
| padding: 40px 20px; | |
| font-size: 0.9em; | |
| } | |
| .reel-drop-hint { | |
| text-align: center; | |
| color: #667eea; | |
| padding: 20px; | |
| font-size: 0.85em; | |
| background: #f8f9ff; | |
| border-radius: 8px; | |
| margin-bottom: 10px; | |
| border: 2px dashed #667eea; | |
| } | |
| h1 { | |
| text-align: center; | |
| color: #333; | |
| margin-bottom: 30px; | |
| font-size: 2em; | |
| } | |
| .drop-zone { | |
| border: 3px dashed #667eea; | |
| border-radius: 15px; | |
| padding: 60px 20px; | |
| text-align: center; | |
| background: #f8f9ff; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| margin-bottom: 30px; | |
| } | |
| .drop-zone.dragover { | |
| background: #e8ebff; | |
| border-color: #764ba2; | |
| transform: scale(1.02); | |
| } | |
| .drop-zone-icon { | |
| font-size: 4em; | |
| margin-bottom: 20px; | |
| } | |
| .drop-zone-text { | |
| color: #667eea; | |
| font-size: 1.2em; | |
| font-weight: 500; | |
| } | |
| .drop-zone-subtext { | |
| color: #999; | |
| margin-top: 10px; | |
| font-size: 0.9em; | |
| } | |
| .drop-zone-hint { | |
| color: #764ba2; | |
| margin-top: 15px; | |
| font-size: 0.85em; | |
| font-weight: 500; | |
| } | |
| .video-container { | |
| display: none; | |
| margin-top: 20px; | |
| position: relative; | |
| } | |
| .video-container.active { | |
| display: block; | |
| } | |
| .video-wrapper { | |
| position: relative; | |
| width: 100%; | |
| resize: both; | |
| overflow: hidden; | |
| min-width: 300px; | |
| min-height: 200px; | |
| max-width: 100%; | |
| } | |
| .video-wrapper-resizable { | |
| border: 2px solid transparent; | |
| border-radius: 12px; | |
| transition: border-color 0.2s ease; | |
| } | |
| .video-wrapper-resizable:hover { | |
| border-color: rgba(102, 126, 234, 0.3); | |
| } | |
| video { | |
| width: 100%; | |
| height: 100%; | |
| border-radius: 10px; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); | |
| background: #000; | |
| display: block; | |
| object-fit: contain; | |
| } | |
| #preloadVideo { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| opacity: 0; | |
| pointer-events: none; | |
| width: 100%; | |
| border-radius: 10px; | |
| } | |
| .video-transition { | |
| transition: opacity 0.3s ease-in-out; | |
| } | |
| .video-info { | |
| margin-top: 15px; | |
| padding: 15px; | |
| background: #f8f9ff; | |
| border-radius: 10px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .video-name { | |
| color: #333; | |
| font-weight: 500; | |
| flex: 1; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .video-controls { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .change-video-btn, .resize-btn { | |
| background: #667eea; | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 0.9em; | |
| font-weight: 500; | |
| transition: background 0.3s ease; | |
| white-space: nowrap; | |
| } | |
| .change-video-btn:hover, .resize-btn:hover { | |
| background: #764ba2; | |
| } | |
| .resize-btn.active { | |
| background: #764ba2; | |
| } | |
| .resize-corner { | |
| position: absolute; | |
| bottom: 5px; | |
| right: 5px; | |
| width: 20px; | |
| height: 20px; | |
| cursor: nwse-resize; | |
| z-index: 100; | |
| } | |
| .resize-corner::before { | |
| content: ''; | |
| position: absolute; | |
| bottom: 2px; | |
| right: 2px; | |
| width: 0; | |
| height: 0; | |
| border-style: solid; | |
| border-width: 0 0 12px 12px; | |
| border-color: transparent transparent rgba(102, 126, 234, 0.5) transparent; | |
| } | |
| .resize-corner:hover::before { | |
| border-color: transparent transparent rgba(102, 126, 234, 0.8) transparent; | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-wrapper"> | |
| <div class="container" id="mainContainer"> | |
| <div class="resize-handle resize-handle-right" id="resizeHandle"></div> | |
| <h1>🎬 Local Video Player</h1> | |
| <div class="drop-zone" id="dropZone"> | |
| <div class="drop-zone-icon">📹</div> | |
| <div class="drop-zone-text">Drag & Drop your videos here</div> | |
| <div class="drop-zone-subtext">or click to browse</div> | |
| <div class="drop-zone-hint">💡 Select multiple videos to create a reel</div> | |
| </div> | |
| <input type="file" id="fileInput" accept="video/*" multiple class="hidden"> | |
| <div class="video-container" id="videoContainer"> | |
| <div class="video-wrapper" id="videoWrapper"> | |
| <video id="videoPlayer" controls></video> | |
| <video id="preloadVideo" muted></video> | |
| <div class="resize-corner" id="resizeCorner"></div> | |
| </div> | |
| <div class="video-info"> | |
| <span class="video-name" id="videoName"></span> | |
| <div class="video-controls"> | |
| <button class="resize-btn" id="resizeBtn">📐 Resize</button> | |
| <button class="change-video-btn" id="changeVideoBtn">Add More Videos</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="reel-sidebar" id="reelSidebar"> | |
| <div class="reel-header"> | |
| <span class="reel-title">📼 Video Reel</span> | |
| <span class="reel-count" id="reelCount">0</span> | |
| </div> | |
| <div class="reel-drop-hint" id="reelDropHint"> | |
| 💡 Drop videos here or drag to reorder | |
| </div> | |
| <div class="reel-list" id="reelList"> | |
| <div class="reel-empty">Drop videos to start building your reel</div> | |
| </div> | |
| <div class="reel-controls"> | |
| <div class="loop-toggle"> | |
| <input type="checkbox" id="loopToggle" checked> | |
| <label for="loopToggle">Loop Reel</label> | |
| </div> | |
| <button class="clear-reel-btn" id="clearReelBtn">Clear All</button> | |
| </div> | |
| <div class="ffmpeg-section" id="ffmpegSection"> | |
| <div class="ffmpeg-title">⚙️ FFmpeg Command</div> | |
| <div id="ffmpegContent"> | |
| <div class="ffmpeg-empty">Add videos to generate ffmpeg command</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const dropZone = document.getElementById('dropZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const videoContainer = document.getElementById('videoContainer'); | |
| const videoPlayer = document.getElementById('videoPlayer'); | |
| const preloadVideo = document.getElementById('preloadVideo'); | |
| const videoName = document.getElementById('videoName'); | |
| const changeVideoBtn = document.getElementById('changeVideoBtn'); | |
| const reelList = document.getElementById('reelList'); | |
| const reelCount = document.getElementById('reelCount'); | |
| const loopToggle = document.getElementById('loopToggle'); | |
| const clearReelBtn = document.getElementById('clearReelBtn'); | |
| const reelSidebar = document.getElementById('reelSidebar'); | |
| const reelDropHint = document.getElementById('reelDropHint'); | |
| const mainContainer = document.getElementById('mainContainer'); | |
| const resizeHandle = document.getElementById('resizeHandle'); | |
| const videoWrapper = document.getElementById('videoWrapper'); | |
| const resizeCorner = document.getElementById('resizeCorner'); | |
| const resizeBtn = document.getElementById('resizeBtn'); | |
| const ffmpegContent = document.getElementById('ffmpegContent'); | |
| // State | |
| let videoReel = []; | |
| let currentVideoIndex = 0; | |
| let draggedItemIndex = null; | |
| let isTransitioning = false; | |
| let isResizing = false; | |
| let resizeMode = 'none'; // 'container' or 'video' | |
| let startX, startY, startWidth, startHeight; | |
| // Prevent default drag behaviors | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, preventDefaults, false); | |
| reelSidebar.addEventListener(eventName, preventDefaults, false); | |
| document.body.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| // Highlight drop zone when dragging over it | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, () => { | |
| dropZone.classList.add('dragover'); | |
| }, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, () => { | |
| dropZone.classList.remove('dragover'); | |
| }, false); | |
| }); | |
| // Highlight reel sidebar when dragging files over it | |
| reelSidebar.addEventListener('dragenter', (e) => { | |
| // Only highlight if dragging files from outside (not reordering) | |
| if (e.dataTransfer.types.includes('Files')) { | |
| reelSidebar.classList.add('dragover'); | |
| } | |
| }); | |
| reelSidebar.addEventListener('dragover', (e) => { | |
| if (e.dataTransfer.types.includes('Files')) { | |
| reelSidebar.classList.add('dragover'); | |
| } | |
| }); | |
| reelSidebar.addEventListener('dragleave', (e) => { | |
| // Check if we're leaving the sidebar entirely | |
| if (e.target === reelSidebar || !reelSidebar.contains(e.relatedTarget)) { | |
| reelSidebar.classList.remove('dragover'); | |
| } | |
| }); | |
| reelSidebar.addEventListener('drop', (e) => { | |
| reelSidebar.classList.remove('dragover'); | |
| if (e.dataTransfer.types.includes('Files')) { | |
| handleDrop(e); | |
| } | |
| }); | |
| // Handle dropped files | |
| dropZone.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| handleFiles(files); | |
| } | |
| // Handle click to browse | |
| dropZone.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| handleFiles(e.target.files); | |
| }); | |
| // Handle add more videos button | |
| changeVideoBtn.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| // Handle clear reel button | |
| clearReelBtn.addEventListener('click', () => { | |
| if (videoReel.length > 0 && confirm('Are you sure you want to clear all videos?')) { | |
| videoReel = []; | |
| currentVideoIndex = 0; | |
| updateReelUI(); | |
| videoPlayer.pause(); | |
| videoPlayer.src = ''; | |
| dropZone.style.display = 'block'; | |
| videoContainer.classList.remove('active'); | |
| } | |
| }); | |
| function handleFiles(files) { | |
| const videoFiles = Array.from(files).filter(file => file.type.startsWith('video/')); | |
| if (videoFiles.length === 0) { | |
| alert('Please select valid video files.'); | |
| return; | |
| } | |
| const wasEmpty = videoReel.length === 0; | |
| // Add videos to reel | |
| videoFiles.forEach(file => { | |
| const videoData = { | |
| id: Date.now() + Math.random(), | |
| file: file, | |
| name: file.name, | |
| url: URL.createObjectURL(file), | |
| duration: null | |
| }; | |
| videoReel.push(videoData); | |
| }); | |
| updateReelUI(); | |
| // If this is the first video, start playing | |
| if (wasEmpty) { | |
| currentVideoIndex = 0; | |
| playVideo(0); | |
| } else { | |
| // If we added videos while playing, update preload | |
| preloadNextVideo(); | |
| } | |
| } | |
| function updateReelUI() { | |
| reelCount.textContent = videoReel.length; | |
| if (videoReel.length === 0) { | |
| reelList.innerHTML = '<div class="reel-empty">Drop videos to start building your reel</div>'; | |
| return; | |
| } | |
| reelList.innerHTML = ''; | |
| videoReel.forEach((video, index) => { | |
| const reelItem = document.createElement('div'); | |
| reelItem.className = 'reel-item'; | |
| reelItem.draggable = true; | |
| reelItem.dataset.index = index; | |
| if (index === currentVideoIndex) { | |
| reelItem.classList.add('active'); | |
| } | |
| const itemName = document.createElement('div'); | |
| itemName.className = 'reel-item-name'; | |
| itemName.textContent = `${index + 1}. ${video.name}`; | |
| const itemDuration = document.createElement('div'); | |
| itemDuration.className = 'reel-item-duration'; | |
| itemDuration.textContent = video.duration ? formatDuration(video.duration) : 'Loading...'; | |
| const removeBtn = document.createElement('button'); | |
| removeBtn.className = 'reel-item-remove'; | |
| removeBtn.textContent = '×'; | |
| removeBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| removeVideo(index); | |
| }; | |
| reelItem.appendChild(itemName); | |
| reelItem.appendChild(itemDuration); | |
| reelItem.appendChild(removeBtn); | |
| // Click to play | |
| reelItem.onclick = (e) => { | |
| if (e.target !== removeBtn) { | |
| playVideo(index); | |
| } | |
| }; | |
| // Drag and drop for reordering | |
| reelItem.addEventListener('dragstart', handleDragStart); | |
| reelItem.addEventListener('dragend', handleDragEnd); | |
| reelItem.addEventListener('dragover', handleDragOver); | |
| reelItem.addEventListener('drop', handleReelItemDrop); | |
| reelItem.addEventListener('dragleave', handleDragLeave); | |
| reelList.appendChild(reelItem); | |
| }); | |
| // Update ffmpeg command | |
| updateFFmpegCommand(); | |
| } | |
| function updateFFmpegCommand() { | |
| if (videoReel.length === 0) { | |
| ffmpegContent.innerHTML = '<div class="ffmpeg-empty">Add videos to generate ffmpeg command</div>'; | |
| return; | |
| } | |
| // Generate mylist.txt content | |
| const mylistContent = videoReel.map(video => `file '${video.name}'`).join('\n'); | |
| // Create the HTML for the ffmpeg section | |
| const html = ` | |
| <div class="ffmpeg-command"> | |
| <button class="copy-btn" onclick="copyToClipboard('ffmpeg-cmd', this)">Copy</button> | |
| <code id="ffmpeg-cmd">ffmpeg -f concat -safe 0 -i mylist.txt -c copy output.mp4</code> | |
| </div> | |
| <div class="mylist-section"> | |
| <div class="mylist-title">📄 mylist.txt</div> | |
| <div class="mylist-content"> | |
| <button class="copy-btn" onclick="copyToClipboard('mylist-content', this)">Copy</button> | |
| <code id="mylist-content">${mylistContent}</code> | |
| </div> | |
| </div> | |
| `; | |
| ffmpegContent.innerHTML = html; | |
| } | |
| // Copy to clipboard function | |
| window.copyToClipboard = function(elementId, button) { | |
| const element = document.getElementById(elementId); | |
| const text = element.textContent; | |
| navigator.clipboard.writeText(text).then(() => { | |
| const originalText = button.textContent; | |
| button.textContent = '✓ Copied'; | |
| button.classList.add('copied'); | |
| setTimeout(() => { | |
| button.textContent = originalText; | |
| button.classList.remove('copied'); | |
| }, 2000); | |
| }).catch(err => { | |
| console.error('Failed to copy:', err); | |
| alert('Failed to copy to clipboard'); | |
| }); | |
| } | |
| // Drag and drop handlers for reordering | |
| function handleDragStart(e) { | |
| draggedItemIndex = parseInt(e.target.dataset.index); | |
| e.target.classList.add('dragging'); | |
| e.dataTransfer.effectAllowed = 'move'; | |
| e.dataTransfer.setData('text/html', e.target.innerHTML); | |
| } | |
| function handleDragEnd(e) { | |
| e.target.classList.remove('dragging'); | |
| // Remove all drag-over classes | |
| document.querySelectorAll('.reel-item').forEach(item => { | |
| item.classList.remove('drag-over'); | |
| }); | |
| } | |
| function handleDragOver(e) { | |
| if (draggedItemIndex === null) return; | |
| e.preventDefault(); | |
| e.dataTransfer.dropEffect = 'move'; | |
| const targetIndex = parseInt(e.currentTarget.dataset.index); | |
| if (targetIndex !== draggedItemIndex) { | |
| e.currentTarget.classList.add('drag-over'); | |
| } | |
| } | |
| function handleDragLeave(e) { | |
| e.currentTarget.classList.remove('drag-over'); | |
| } | |
| function handleReelItemDrop(e) { | |
| if (draggedItemIndex === null) return; | |
| e.stopPropagation(); | |
| e.currentTarget.classList.remove('drag-over'); | |
| const targetIndex = parseInt(e.currentTarget.dataset.index); | |
| if (draggedItemIndex !== targetIndex) { | |
| // Reorder the array | |
| const draggedVideo = videoReel[draggedItemIndex]; | |
| videoReel.splice(draggedItemIndex, 1); | |
| videoReel.splice(targetIndex, 0, draggedVideo); | |
| // Update current video index | |
| if (currentVideoIndex === draggedItemIndex) { | |
| currentVideoIndex = targetIndex; | |
| } else if (draggedItemIndex < currentVideoIndex && targetIndex >= currentVideoIndex) { | |
| currentVideoIndex--; | |
| } else if (draggedItemIndex > currentVideoIndex && targetIndex <= currentVideoIndex) { | |
| currentVideoIndex++; | |
| } | |
| updateReelUI(); | |
| } | |
| draggedItemIndex = null; | |
| } | |
| function playVideo(index, autoplay = true) { | |
| if (index < 0 || index >= videoReel.length || isTransitioning) return; | |
| currentVideoIndex = index; | |
| const video = videoReel[index]; | |
| // Set the video source | |
| videoPlayer.src = video.url; | |
| videoName.textContent = video.name; | |
| // Show video container and hide drop zone | |
| dropZone.style.display = 'none'; | |
| videoContainer.classList.add('active'); | |
| // Update duration when metadata is loaded | |
| if (!video.duration) { | |
| videoPlayer.addEventListener('loadedmetadata', () => { | |
| video.duration = videoPlayer.duration; | |
| updateReelUI(); | |
| }, { once: true }); | |
| } | |
| // Preload metadata for smoother start | |
| videoPlayer.preload = 'auto'; | |
| // Play video when ready | |
| if (autoplay) { | |
| videoPlayer.play().catch(err => { | |
| console.log('Autoplay prevented:', err); | |
| }); | |
| } | |
| updateReelUI(); | |
| // Preload next video | |
| preloadNextVideo(); | |
| } | |
| function preloadNextVideo() { | |
| if (videoReel.length === 0) return; | |
| let nextIndex = currentVideoIndex + 1; | |
| // Handle looping | |
| if (nextIndex >= videoReel.length) { | |
| if (loopToggle.checked) { | |
| nextIndex = 0; | |
| } else { | |
| return; // Don't preload if not looping and at the end | |
| } | |
| } | |
| const nextVideo = videoReel[nextIndex]; | |
| if (nextVideo) { | |
| preloadVideo.src = nextVideo.url; | |
| preloadVideo.preload = 'auto'; | |
| preloadVideo.load(); | |
| // Update duration for next video if not set | |
| if (!nextVideo.duration) { | |
| preloadVideo.addEventListener('loadedmetadata', () => { | |
| nextVideo.duration = preloadVideo.duration; | |
| updateReelUI(); | |
| }, { once: true }); | |
| } | |
| } | |
| } | |
| function transitionToNextVideo() { | |
| if (videoReel.length === 0 || isTransitioning) return; | |
| let nextIndex = currentVideoIndex + 1; | |
| // If we've reached the end | |
| if (nextIndex >= videoReel.length) { | |
| if (loopToggle.checked) { | |
| nextIndex = 0; | |
| } else { | |
| // Stop at the end | |
| currentVideoIndex = videoReel.length - 1; | |
| return; | |
| } | |
| } | |
| isTransitioning = true; | |
| // Swap the videos for seamless transition | |
| const nextVideo = videoReel[nextIndex]; | |
| // Update current index and UI | |
| currentVideoIndex = nextIndex; | |
| videoName.textContent = nextVideo.name; | |
| updateReelUI(); | |
| // Quick switch to preloaded video | |
| videoPlayer.src = nextVideo.url; | |
| videoPlayer.currentTime = 0; | |
| videoPlayer.play().then(() => { | |
| isTransitioning = false; | |
| // Preload the next video after this one | |
| preloadNextVideo(); | |
| }).catch(err => { | |
| console.log('Playback error:', err); | |
| isTransitioning = false; | |
| }); | |
| } | |
| function removeVideo(index) { | |
| if (index < 0 || index >= videoReel.length) return; | |
| // Revoke object URL to free memory | |
| URL.revokeObjectURL(videoReel[index].url); | |
| videoReel.splice(index, 1); | |
| if (videoReel.length === 0) { | |
| videoPlayer.pause(); | |
| videoPlayer.src = ''; | |
| dropZone.style.display = 'block'; | |
| videoContainer.classList.remove('active'); | |
| currentVideoIndex = 0; | |
| } else if (index === currentVideoIndex) { | |
| // If we removed the current video, play the next one (or previous if it was the last) | |
| currentVideoIndex = Math.min(index, videoReel.length - 1); | |
| playVideo(currentVideoIndex); | |
| } else if (index < currentVideoIndex) { | |
| // Adjust current index if we removed a video before it | |
| currentVideoIndex--; | |
| } | |
| updateReelUI(); | |
| } | |
| function formatDuration(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| // Handle video end - play next video seamlessly | |
| videoPlayer.addEventListener('ended', () => { | |
| transitionToNextVideo(); | |
| }); | |
| // Preload next video when current video is halfway through | |
| videoPlayer.addEventListener('timeupdate', () => { | |
| if (videoPlayer.duration && videoPlayer.currentTime > videoPlayer.duration / 2) { | |
| // Ensure next video is preloaded by halfway point | |
| if (!preloadVideo.src || preloadVideo.readyState < 2) { | |
| preloadNextVideo(); | |
| } | |
| } | |
| }); | |
| // ===== RESIZE FUNCTIONALITY ===== | |
| // Toggle resize mode | |
| resizeBtn.addEventListener('click', () => { | |
| if (resizeMode === 'video') { | |
| // Disable video resize mode | |
| resizeMode = 'none'; | |
| resizeBtn.classList.remove('active'); | |
| videoWrapper.classList.remove('video-wrapper-resizable'); | |
| videoWrapper.style.resize = 'none'; | |
| resizeCorner.style.display = 'none'; | |
| } else { | |
| // Enable video resize mode | |
| resizeMode = 'video'; | |
| resizeBtn.classList.add('active'); | |
| videoWrapper.classList.add('video-wrapper-resizable'); | |
| videoWrapper.style.resize = 'both'; | |
| resizeCorner.style.display = 'block'; | |
| } | |
| }); | |
| // Container resize (drag right edge) | |
| resizeHandle.addEventListener('mousedown', (e) => { | |
| isResizing = true; | |
| resizeMode = 'container'; | |
| startX = e.clientX; | |
| startWidth = mainContainer.offsetWidth; | |
| mainContainer.classList.add('resizing'); | |
| e.preventDefault(); | |
| }); | |
| // Video resize (drag corner) | |
| resizeCorner.addEventListener('mousedown', (e) => { | |
| isResizing = true; | |
| resizeMode = 'video'; | |
| startX = e.clientX; | |
| startY = e.clientY; | |
| startWidth = videoWrapper.offsetWidth; | |
| startHeight = videoWrapper.offsetHeight; | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }); | |
| // Mouse move handler | |
| document.addEventListener('mousemove', (e) => { | |
| if (!isResizing) return; | |
| if (resizeMode === 'container') { | |
| const deltaX = e.clientX - startX; | |
| const newWidth = startWidth + deltaX; | |
| // Set min and max width | |
| if (newWidth >= 400 && newWidth <= window.innerWidth - 400) { | |
| mainContainer.style.flex = `0 0 ${newWidth}px`; | |
| } | |
| } else if (resizeMode === 'video') { | |
| const deltaX = e.clientX - startX; | |
| const deltaY = e.clientY - startY; | |
| const newWidth = startWidth + deltaX; | |
| const newHeight = startHeight + deltaY; | |
| // Set min dimensions | |
| if (newWidth >= 300) { | |
| videoWrapper.style.width = `${newWidth}px`; | |
| } | |
| if (newHeight >= 200) { | |
| videoWrapper.style.height = `${newHeight}px`; | |
| } | |
| } | |
| }); | |
| // Mouse up handler | |
| document.addEventListener('mouseup', () => { | |
| if (isResizing) { | |
| isResizing = false; | |
| mainContainer.classList.remove('resizing'); | |
| if (resizeMode === 'container') { | |
| resizeMode = 'none'; | |
| } | |
| } | |
| }); | |
| // Prevent text selection while resizing | |
| document.addEventListener('selectstart', (e) => { | |
| if (isResizing) { | |
| e.preventDefault(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment