| const pageLabel = document.getElementById("pageLabel"); |
| const statusText = document.getElementById("status"); |
| const slideImage = document.getElementById("slideImage"); |
| const imageFallback = document.getElementById("imageFallback"); |
| const slideTitle = document.getElementById("slideTitle"); |
| const slideScript = document.getElementById("slideScript"); |
|
|
| const prevBtn = document.getElementById("prevBtn"); |
| const playBtn = document.getElementById("playBtn"); |
| const stopBtn = document.getElementById("stopBtn"); |
| const nextBtn = document.getElementById("nextBtn"); |
| const player = document.getElementById("player"); |
|
|
| const workspace = document.getElementById("workspace"); |
| const slidePane = document.getElementById("slidePane"); |
| const scriptPane = document.getElementById("scriptPane"); |
| const resizer = document.getElementById("resizer"); |
|
|
| let slides = []; |
| let currentIndex = 0; |
|
|
| let isPlaying = false; |
| let isPaused = false; |
|
|
| let isDragging = false; |
| let pointerId = null; |
|
|
| function setStatus(text) { |
| statusText.textContent = text; |
| } |
|
|
| function updatePlayButton() { |
| if (isPlaying && !isPaused) { |
| playBtn.textContent = "⏸ Pause"; |
| return; |
| } |
|
|
| if (isPlaying && isPaused) { |
| playBtn.textContent = "▶ Resume"; |
| return; |
| } |
|
|
| playBtn.textContent = "▶ Play"; |
| } |
|
|
| function updateButtons() { |
| prevBtn.disabled = currentIndex <= 0; |
| nextBtn.disabled = currentIndex >= slides.length - 1; |
| } |
|
|
| function renderSlide(index) { |
| if (!slides.length) { |
| pageLabel.textContent = "No slides"; |
| slideTitle.textContent = ""; |
| slideScript.textContent = "No script data was loaded."; |
| setStatus("No slides"); |
| updateButtons(); |
| return; |
| } |
|
|
| if (index < 0) index = 0; |
| if (index >= slides.length) index = slides.length - 1; |
|
|
| currentIndex = index; |
|
|
| const slide = slides[currentIndex]; |
|
|
| pageLabel.textContent = `Slide ${currentIndex} / ${slides.length - 1}`; |
| slideTitle.textContent = slide.title || `Slide ${currentIndex}`; |
| slideScript.textContent = slide.speech || ""; |
|
|
| imageFallback.hidden = true; |
| slideImage.hidden = false; |
|
|
| slideImage.onerror = () => { |
| slideImage.hidden = true; |
| imageFallback.hidden = false; |
| }; |
|
|
| slideImage.src = `${slide.image}?t=${Date.now()}`; |
|
|
| updateButtons(); |
|
|
| if (!isPlaying) { |
| setStatus("Ready"); |
| } |
| } |
|
|
| async function loadSlides() { |
| try { |
| const response = await fetch("/api/slides"); |
|
|
| if (!response.ok) { |
| throw new Error(`Failed to load slides: ${response.status}`); |
| } |
|
|
| slides = await response.json(); |
|
|
| if (!Array.isArray(slides) || slides.length === 0) { |
| throw new Error("No slides found. Check scripts.txt."); |
| } |
|
|
| renderSlide(0); |
| setStatus(`Ready · ${slides.length} slides`); |
| updatePlayButton(); |
|
|
| } catch (error) { |
| console.error(error); |
|
|
| pageLabel.textContent = "Load failed"; |
| slideTitle.textContent = "Error"; |
| slideScript.textContent = String(error.message || error); |
|
|
| setStatus("Error"); |
| } |
| } |
|
|
| function stopAudio(status = "Stopped") { |
|
|
| isPlaying = false; |
| isPaused = false; |
|
|
| player.pause(); |
| player.removeAttribute("src"); |
| player.load(); |
|
|
| setStatus(status); |
| updatePlayButton(); |
| } |
|
|
| function playCurrent() { |
|
|
| if (!slides.length) return; |
|
|
| isPlaying = true; |
| isPaused = false; |
|
|
| const slide = slides[currentIndex]; |
|
|
| setStatus(`Reading ${currentIndex + 1}/${slides.length}`); |
|
|
| player.pause(); |
| player.currentTime = 0; |
|
|
| player.src = `${slide.audio}?t=${Date.now()}`; |
|
|
| updatePlayButton(); |
|
|
| player.play().catch(error => { |
| console.error(error); |
| stopAudio("Audio play failed"); |
| }); |
| } |
|
|
| function goToSlide(index, keepPlaying = false) { |
|
|
| const wasPlaying = |
| (isPlaying && !isPaused) || keepPlaying; |
|
|
| player.pause(); |
| player.currentTime = 0; |
|
|
| renderSlide(index); |
|
|
| if (wasPlaying) { |
| playCurrent(); |
| } else { |
| isPlaying = false; |
| isPaused = false; |
| updatePlayButton(); |
| } |
| } |
|
|
| prevBtn.addEventListener("click", () => { |
| if (currentIndex <= 0) return; |
| goToSlide(currentIndex - 1, isPlaying && !isPaused); |
| }); |
|
|
| nextBtn.addEventListener("click", () => { |
| if (currentIndex >= slides.length - 1) return; |
| goToSlide(currentIndex + 1, isPlaying && !isPaused); |
| }); |
|
|
| playBtn.addEventListener("click", () => { |
|
|
| if (!isPlaying) { |
| playCurrent(); |
| return; |
| } |
|
|
| if (isPlaying && !isPaused) { |
|
|
| player.pause(); |
|
|
| isPaused = true; |
|
|
| setStatus("Paused"); |
| updatePlayButton(); |
|
|
| return; |
| } |
|
|
| if (isPlaying && isPaused) { |
|
|
| player.play() |
| .then(() => { |
|
|
| isPaused = false; |
|
|
| setStatus( |
| `Reading ${currentIndex + 1}/${slides.length}` |
| ); |
|
|
| updatePlayButton(); |
| }) |
| .catch(error => { |
|
|
| console.error(error); |
|
|
| stopAudio( |
| "Audio play failed" |
| ); |
| }); |
| } |
| }); |
|
|
| stopBtn.addEventListener("click", () => { |
| stopAudio("Stopped"); |
| }); |
|
|
| player.addEventListener("ended", () => { |
|
|
| if (!isPlaying) return; |
| if (isPaused) return; |
|
|
| if (currentIndex < slides.length - 1) { |
|
|
| goToSlide( |
| currentIndex + 1, |
| true |
| ); |
|
|
| } else { |
|
|
| stopAudio( |
| "Finished" |
| ); |
| } |
| }); |
|
|
| document.addEventListener("keydown", event => { |
|
|
| if (event.key === "ArrowLeft") { |
| prevBtn.click(); |
| } |
|
|
| if (event.key === "ArrowRight") { |
| nextBtn.click(); |
| } |
|
|
| if (event.key === " ") { |
|
|
| event.preventDefault(); |
|
|
| playBtn.click(); |
| } |
| }); |
|
|
| function startResize(event) { |
|
|
| if (window.innerWidth <= 900) |
| return; |
|
|
| isDragging = true; |
|
|
| pointerId = event.pointerId; |
|
|
| document.body.classList.add( |
| "dragging" |
| ); |
|
|
| try { |
|
|
| resizer.setPointerCapture( |
| pointerId |
| ); |
|
|
| } catch (error) { |
| |
| } |
| } |
|
|
| function resizePanes(clientX) { |
|
|
| if (!isDragging) |
| return; |
|
|
| if (window.innerWidth <= 900) |
| return; |
|
|
| const rect = |
| workspace.getBoundingClientRect(); |
|
|
| const x = |
| clientX - rect.left; |
|
|
| const percent = |
| (x / rect.width) * 100; |
|
|
| const scriptWidth = |
| Math.max( |
| 18, |
| Math.min( |
| 58, |
| percent |
| ) |
| ); |
|
|
| const slideWidth = |
| 100 - scriptWidth; |
|
|
| scriptPane.style.flexBasis = |
| `${scriptWidth}%`; |
|
|
| scriptPane.style.width = |
| `${scriptWidth}%`; |
|
|
| slidePane.style.flexBasis = |
| `${slideWidth}%`; |
| } |
|
|
| function stopResize() { |
|
|
| if (!isDragging) |
| return; |
|
|
| isDragging = false; |
|
|
| pointerId = null; |
|
|
| document.body.classList.remove( |
| "dragging" |
| ); |
| } |
|
|
| resizer.addEventListener( |
| "pointerdown", |
| event => { |
| startResize(event); |
| } |
| ); |
|
|
| window.addEventListener( |
| "pointermove", |
| event => { |
| resizePanes(event.clientX); |
| } |
| ); |
|
|
| window.addEventListener( |
| "pointerup", |
| () => { |
| stopResize(); |
| } |
| ); |
|
|
| window.addEventListener( |
| "pointercancel", |
| () => { |
| stopResize(); |
| } |
| ); |
|
|
| window.addEventListener( |
| "resize", |
| () => { |
|
|
| if ( |
| window.innerWidth <= 900 |
| ) { |
|
|
| slidePane.style.flexBasis = ""; |
| scriptPane.style.flexBasis = ""; |
| scriptPane.style.width = ""; |
|
|
| stopResize(); |
| } |
| } |
| ); |
|
|
| loadSlides(); |