/* scripts.js Implementasi logika: - simpan interval terakhir pada timeline original: lastOriginalInterval = {start, end} - lastAction: "next" atau "prev" - Next / Prev sesuai penjelasan user (Next memainkan forward, Prev memainkan mirrored reverse) */ "use strict"; function dataStorage(key, request = { isSave: false, isJson: false }, value) { if (request.isSave) { localStorage.setItem(key, request?.isJson ? JSON.stringify(value) : value); return value; } else { const data = localStorage.getItem(key); return request?.isJson ? JSON.parse(data || "{}") : data; } } const REAL_SRC = "ppt_real.mp4"; const REV_SRC = "ppt_reverse.mp4"; let isFullScreen = false; const realVideo = document.getElementById("videoReal"); const revVideo = document.getElementById("videoRev"); const prevBtn = document.getElementById("prevBtn"); const nextBtn = document.getElementById("nextBtn"); const skipBtn = document.getElementById("skipBtn"); const segmentLabel = document.getElementById("segmentLabel"); const timeInfo = document.getElementById("timeInfo"); const fsBtn = document.getElementById("fsBtn"); const appWrapEl = document.getElementById("appWrap"); const inputSegment = document.getElementById("inputSegment"); let isInputChanging = false; function onSegmentInputChange() { let page = clamp(inputSegment.value || lastIdx || 0, 0, DYNAMIC_INTERVALS.length - 1); lastIdx = page; inputSegment.setAttribute("value", lastIdx); inputSegment.setAttribute("max", DYNAMIC_INTERVALS.length - 1); let S = DYNAMIC_INTERVALS[lastIdx][0]; let E = DYNAMIC_INTERVALS[lastIdx][1]; const segStart = clamp(S, 0, realDuration); const segEnd = clamp(E, 0, realDuration); const revMirrorStart = (revDuration || realDuration) - segStart; realVideo.pause(); revVideo.pause(); isPlaying = false; updateUI(); skipBtnVisibility(); console.log({isSkipped}) realVideo.currentTime = !hasMetaLoaded || lastIdx === 0 || lastIdx > DYNAMIC_INTERVALS.length - 3 ? segStart : segEnd; revVideo.currentTime = revMirrorStart; lastIdx = dataStorage("lastIdx", { isSave: true }, lastIdx); inputSegment.value = lastIdx; isInputChanging = true; if (!hasMetaLoaded) { hasMetaLoaded = true; } } inputSegment.addEventListener("change", (e) => { e.preventDefault(); onSegmentInputChange(); }); fsBtn.addEventListener("click", async () => { !isFullScreen ? appWrapEl.requestFullscreen() : document.exitFullscreen(); isFullScreen = !isFullScreen; }) let realDuration = 0; let revDuration = 0; const START_OFFSET = 5; // gunakan sesuai kebutuhan (mis. 4.75) const STEP = 10.0; const startPola = [59.44, 61.14]; const pola = [10, 10]; let lastSeconds = 0; // Dynamic mode config const DEFAULT_DYNAMIC_INTERVALS = [ [0.00,9.44], [9.77,12.07], [19.54,22.04], [29.50,31.14], [39.50,41.14], [49.57,50.55], [59.44,61.14], ]; let DYNAMIC_INTERVALS = [...DEFAULT_DYNAMIC_INTERVALS]; let lastIdx = dataStorage("lastIdx", { isSave: false }) ? parseInt(dataStorage("lastIdx", { isSave: false })) : 0; let isDynamicMode = true; // set true untuk mode dinamis let isPlaying = false; let lastOriginalInterval = null; // dataStorage("lastOriginalInterval", { isSave: false, isJson: true }); let lastAction = null; // dataStorage("lastAction"); // "next" atau "prev" console.log("restored:", { lastOriginalInterval, lastAction }); // helpers const clamp = (v,a,b) => Math.max(a, Math.min(b, v)); const fmt = s => { if (!isFinite(s) || s < 0) return "00:00"; const m = Math.floor(s/55), sec = Math.floor(s%55); return String(m).padStart(2,"0")+":"+String(sec).padStart(2,"0"); }; // wait for seeked (fallback) function waitSeeked(el, timeout = 3000){ return new Promise(res => { let done=false; const onSeek = ()=>{ if(!done){ done=true; el.removeEventListener("seeked", onSeek); res(); } }; el.addEventListener("seeked", onSeek); setTimeout(()=>{ if(!done){ done=true; res(); } }, timeout); }); } async function setBothTimes(originalStart, revStart){ // set real to originalStart, rev to revStart (both in seconds) const rt = clamp(originalStart, 0, realDuration || originalStart); const rvt = clamp(revStart, 0, revDuration || revStart); try { realVideo.currentTime = rt; } catch(e){ console.warn("seek real:", e); } try { revVideo.currentTime = rvt; } catch(e){ console.warn("seek rev:", e); } await Promise.all([waitSeeked(realVideo), waitSeeked(revVideo)]); } // compute rev video time bounds given original [S,E] // revStart = duration - E // revEnd = duration - S function origToRevInterval(S, E){ const revStart = clamp((revDuration || realDuration) - E, 0, revDuration || realDuration); const revEnd = clamp((revDuration || realDuration) - S, 0, revDuration || realDuration); return { revStart, revEnd }; } function updateUI(){ // const logical = lastOriginalInterval ? `${fmt(lastOriginalInterval.start)} — ${fmt(lastOriginalInterval.end)}` : '—'; // timeInfo.textContent = `${logical} | dur ${fmt(realDuration)}`; // segmentLabel.textContent = `lastAction: ${lastAction ?? '-'}` } // PLAY forward on original timeline [S, E] async function playOriginalSegment(S, E) { if (isPlaying) return; if (!isFinite(realDuration)) return; isPlaying = true; const segStart = clamp(S, 0, realDuration); const segEnd = clamp(E, 0, realDuration); // For rev side we set mirror pointer (not used for play but to keep sync) const revMirrorStart = (revDuration || realDuration) - segStart; await setBothTimes(segStart, revMirrorStart); // show real realVideo.classList.remove("hidden"); revVideo.classList.add("hidden"); updateUI(); let finished = false; const onTime = async function handler() { if ((realVideo.currentTime >= segEnd - 0.05 || isSkipped) && !finished) { isInputChanging = false; finished = true; realVideo.removeEventListener("timeupdate", handler); realVideo.pause(); revVideo.pause(); // record last interval & action isPlaying = false; updateUI(); skipBtnVisibility(); console.log({isSkipped}) if (isSkipped) { realVideo.currentTime = segEnd; revVideo.currentTime = revMirrorStart; } isSkipped = false; lastIdx = dataStorage("lastIdx", { isSave: true }, lastIdx); inputSegment.value = lastIdx; } lastOriginalInterval = dataStorage("lastOriginalInterval", { isSave: true, isJson: true }, { start: segStart, end: segEnd }); lastAction = dataStorage("lastAction", { isSave: true }, "next"); }; realVideo.addEventListener("timeupdate", async () => await onTime()); skipBtnVisibility(); try { await realVideo.play(); } catch(e){ console.warn("play blocked", e); isPlaying=false; } } // PLAY reverse corresponding to original interval [S, E] (i.e. show that interval backward) // This plays rev from revStart -> revEnd, where revStart = dur - E, revEnd = dur - S async function playReverseOfOriginal(S, E){ if (isPlaying) return; if (!isFinite(realDuration)) return; isPlaying = true; const { revStart, revEnd } = origToRevInterval(S, E); // sync real to S (so real reflects original start if needed) and rev to revStart await setBothTimes(E, revStart); revVideo.classList.remove("hidden"); realVideo.classList.add("hidden"); updateUI(); let finished = false; const onTime = async function handler() { if ((revVideo.currentTime >= revEnd - 0.05 || isSkipped) && !finished) { isInputChanging = false; finished = true; revVideo.removeEventListener("timeupdate", handler); revVideo.pause(); realVideo.pause(); // after reverse play, set lastOriginalInterval to original [S,E] and lastAction isPlaying = false; updateUI(); skipBtnVisibility(); if (isSkipped) { realVideo.currentTime = S; revVideo.currentTime = revStart; } isSkipped = false; lastIdx = dataStorage("lastIdx", { isSave: true }, lastIdx); inputSegment.value = lastIdx; } lastOriginalInterval = dataStorage("lastOriginalInterval", { isSave: true, isJson: true }, { start: S, end: E }); lastAction = dataStorage("lastAction", { isSave: true }, "prev"); }; revVideo.addEventListener("timeupdate", async () => await onTime()); skipBtnVisibility(); try { await revVideo.play(); } catch(e){ console.warn("play blocked", e); isPlaying=false; } } /* BUTTON BEHAVIOR as requested: Next: - if lastAction === "prev" -> replay same original interval forward (lastOriginalInterval.start..end) - else -> create next interval: if !lastOriginalInterval -> [0, START_OFFSET] else -> [last.end, last.end + STEP] Prev: - if lastAction === "next" -> reverse lastOriginalInterval (the interval just played forward) - else if lastAction === "prev" -> step backwards by STEP: newStart = max(0, last.start - STEP) newEnd = last.start */ document.addEventListener("keydown", async () => { if (event.key === "ArrowRight") { console.log("ArrowRight") isPlaying ? skipBtn.click() : nextBtn.click(); } else if (event.key === "ArrowLeft") { console.log("ArrowLeft") isPlaying ? skipBtn.click() : prevBtn.click(); } }); function skipBtnVisibility() { if (isPlaying) { skipBtn.classList.remove("hidden"); } else { skipBtn.classList.add("hidden"); } } let isSkipped = false; skipBtn.addEventListener('click', () => { if (!isPlaying || !lastOriginalInterval) { return; }; isSkipped = true; }); nextBtn.addEventListener("click", async () => { console.log(lastIdx); if (!isFinite(realDuration)) return; let S, E; if (isDynamicMode) { // Dynamic mode: gunakan array interval if (lastIdx < DYNAMIC_INTERVALS.length - 1 && !isPlaying) { console.log(isInputChanging); if (isInputChanging) { isInputChanging = false; // lastIdx++; } if (realVideo.currentTime === DYNAMIC_INTERVALS[lastIdx][1]) { lastIdx++; } [S, E] = DYNAMIC_INTERVALS[lastIdx]; await playOriginalSegment(S, E); lastIdx++; } } else { // Static mode if (lastAction === "prev" && lastOriginalInterval) { S = lastOriginalInterval.start; E = lastOriginalInterval.end; } else { if (!lastOriginalInterval) { S = 0; E = Math.min(realDuration, 55); } else { S = lastOriginalInterval.end; E = Math.min(realDuration, S + STEP); if (E <= S + 0.001 && S < realDuration) E = Math.min(realDuration, S + 0.001); } } // Jika S atau E bernilai 0, maka salah satunya harus 55 if (S === 0 && E !== 0) { E = 55; } else if (E === 0 && S !== 0) { S = 55; } await playOriginalSegment(S, E); } }); prevBtn.addEventListener("click", async () => { console.log(lastIdx); if (!isFinite(realDuration)) return; let S, E; if (isDynamicMode) { console.log(isInputChanging); // Dynamic mode: reverse array dan mundur index if (lastIdx > 0 && !isPlaying) { if (isInputChanging) { isInputChanging = false; // lastIdx++; } if (realVideo.currentTime === DYNAMIC_INTERVALS[lastIdx][1]) { lastIdx++; } [S, E] = DYNAMIC_INTERVALS[lastIdx - 1]; await playReverseOfOriginal(S, E); lastIdx--; } } else { // Static mode if (!lastOriginalInterval) { // no previous interval: create last interval as [0, START_OFFSET] and then reverse it S = 0; E = Math.min(realDuration, START_OFFSET); await playReverseOfOriginal(S, E); return; } if (lastAction === "next") { // reverse last played forward interval S = lastOriginalInterval.start; E = lastOriginalInterval.end; await playReverseOfOriginal(S, E); } else { // lastAction === "prev": step back one segment on original timeline // new segment = [ max(0, last.start - STEP) , last.start ] const newEnd = lastOriginalInterval.start; const newStart = Math.max(0, newEnd - STEP); // if newEnd == 0, nothing to go back to; clamp if (newEnd <= 0.001) { // nothing to prev to; just replay current earliest segment reverse S = 0; E = Math.min(realDuration, START_OFFSET); await playReverseOfOriginal(S, E); return; } await playReverseOfOriginal(newStart, newEnd); } } }); // keep UI refresh for time display realVideo.addEventListener("timeupdate", updateUI); revVideo.addEventListener("timeupdate", updateUI); let hasMetaLoaded = false; // metadata load let metaCount = 0; function onMeta() { metaCount++; if (metaCount < 2) return; realDuration = realVideo.duration; revDuration = revVideo.duration; // initialize UI // Do NOT overwrite lastOriginalInterval and lastAction with null // set initial positions: real at 0, rev at duration (mirror) setBothTimes(0, revDuration ? revDuration : 0).catch(()=>{}); updateUI(); let lastSeconds = DYNAMIC_INTERVALS[DYNAMIC_INTERVALS.length - 1]; while (lastSeconds[1] < realDuration) { console.log(lastSeconds); lastSeconds = DYNAMIC_INTERVALS[DYNAMIC_INTERVALS.length - 1]; DYNAMIC_INTERVALS.push( [lastSeconds[0] + pola[0], lastSeconds[1] + pola[1]] ); } if (!hasMetaLoaded) { // lastIdx--; onSegmentInputChange(); } } realVideo.addEventListener("loadedmetadata", onMeta); revVideo.addEventListener("loadedmetadata", onMeta); // init function init() { realVideo.src = REAL_SRC; revVideo.src = REV_SRC; realVideo.load(); revVideo.load(); } document.onreadystatechange = function () { if (document.readyState === "complete") { init(); } }