您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Overlay with movable skip/speed controls. Custom skip, smooth speed slider, fullscreen support. Play/Pause label replaces 0x and toggles playback (restores last rate).
// ==UserScript== // @name Mobile Video Controller (Movable + Speed Menu + Skip Durations + Speed Slider + Fullscreen) // @namespace https://your.namespace // @version 5.1.0 // @description Overlay with movable skip/speed controls. Custom skip, smooth speed slider, fullscreen support. Play/Pause label replaces 0x and toggles playback (restores last rate). // @match *://*/* // @grant none // ==/UserScript== (function () { 'use strict'; const MIN_FRAC_PAUSED = 0.20; const MIN_FRAC_PLAYING = 0.08; const EDGE = 6; const DRAG_OFFSET_Y = -20; // Fixed menu speeds (0 replaced by Play/Pause menu item) const SPEEDS = [0, 2, 1.75, 1.5, 1.25, 1]; // Definable skip duration options for the long-press menu const SKIP_DURATIONS = [5, 10, 15, 30, 60]; let activeVideo = null; let uiWrap = null; let manualDrag = false; let menu, speedBtn, hideSpeedMenu, backdrop, skipMenu; let longPressDirection = 0; // To store skip direction (-1 or 1) // --- Persistent settings --- const skipKey = "mvc_skip_seconds"; let skipSeconds = Number(localStorage.getItem(skipKey)) || 10; // store last non-zero rate so Play/Pause can restore it const LAST_RATE_KEY = "mvc_last_rate"; if (!localStorage.getItem(LAST_RATE_KEY)) localStorage.setItem(LAST_RATE_KEY, "1"); function saveSkip(v) { skipSeconds = v; localStorage.setItem(skipKey, String(v)); } const clamp = (v, a, b) => Math.max(a, Math.min(b, v)); const clampTime = (v, t) => { const d = Number.isFinite(v.duration) ? v.duration : Infinity; return clamp(t, 0, d); }; // ---------- UI ---------- function createUI() { uiWrap = document.createElement('div'); uiWrap.style.cssText = ` position: fixed; left: 12px; top: 12px; z-index: 2147483647; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; display: none; pointer-events: auto; `; const panel = document.createElement('div'); panel.style.cssText = ` display: flex; flex-direction: column; align-items: center; gap: 2px; background: transparent; color: #fff; border-radius: 2px; touch-action: none; user-select: none; pointer-events: auto; `; const dragHandle = document.createElement('div'); dragHandle.style.cssText = ` width: 60px; height: 10px; border-radius: 3px; background: rgba(200, 200, 200, 0.2); margin-bottom: 4px; cursor: grab; `; const row = document.createElement('div'); row.style.cssText = ` display: flex; align-items: center; gap: 8px; padding: 1px 4px; background: transparent; pointer-events: auto; `; const btnStyle = ` appearance: none; border: 0; border-radius: 10px; padding: 6px 10px; font-size: 20px; font-weight: 600; color: #fff; background: rgba(43,43,43,0.25); min-width: 56px; text-align: center; line-height: 1; pointer-events: auto; `; const mkBtn = (txt, title, minW) => { const b = document.createElement('button'); b.textContent = txt; b.title = title || ''; b.style.cssText = btnStyle; if (minW) b.style.minWidth = minW; ['pointerdown','pointerup','click','touchstart','touchend'].forEach(type => { b.addEventListener(type, e => { e.stopPropagation(); }); }); return b; }; const rewind = mkBtn(`⟲ ${skipSeconds}`, 'Rewind'); speedBtn = mkBtn('1x', 'Playback speed', '72px'); const forward = mkBtn(`${skipSeconds} ⟳`, 'Forward'); // Backdrop backdrop = document.createElement('div'); backdrop.style.cssText = ` display: none; position: fixed; left: 0; top: 0; width: 100%; height: 100%; z-index: 2147483646; background: transparent; pointer-events: auto; touch-action: none; `; document.body.appendChild(backdrop); function hideSkipMenu() { if (skipMenu) skipMenu.style.display = 'none'; if (menu.style.display === 'none') { backdrop.style.display = 'none'; } } ['pointerdown','click','touchstart','touchend'].forEach(ev => { backdrop.addEventListener(ev, e => { e.stopPropagation(); e.preventDefault(); hideSpeedMenu(); hideSkipMenu(); }, true); }); // Speed menu menu = document.createElement('div'); menu.className = 'mvc-speed-menu'; menu.style.cssText = ` display: none; position: fixed; background: rgba(0,0,0,0.50); border-radius: 5px; z-index: 2147483647; min-width: 50px; max-height: 64vh; overflow-y: auto; pointer-events: auto; touch-action: manipulation; -webkit-tap-highlight-color: transparent; `; document.body.append(menu); // Speed options function makeOpt(sp, isFirst) { const opt = document.createElement('div'); opt.textContent = (sp === 0) ? `Play/Pause` : `${sp}x`; opt.dataset.sp = String(sp); opt.style.cssText = ` color: white; padding: 0.45em 0.9em; font-size: 15px; text-align: center; border-top: ${isFirst ? 'none' : '1px solid rgba(255,255,255,0.12)'}; user-select: none; `; opt.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); const spv = Number(opt.dataset.sp); if (!activeVideo) { hideSpeedMenu(); return; } if (spv === 0) { if (activeVideo.paused) { const last = Number(localStorage.getItem(LAST_RATE_KEY)) || 1; try { activeVideo.playbackRate = last; } catch (err) {} try { activeVideo.play(); } catch (err) {} speedBtn.firstChild.nodeValue = `${(last).toFixed(2)}x`.replace(/\.00$/,''); speedBtn.removeAttribute('data-zero'); localStorage.setItem(LAST_RATE_KEY, String(last)); } else { try { localStorage.setItem(LAST_RATE_KEY, String(activeVideo.playbackRate)); } catch (err) {} try { activeVideo.pause(); } catch (err) {} speedBtn.setAttribute('data-zero', '1'); speedBtn.firstChild.nodeValue = 'Play'; } } else { try { activeVideo.playbackRate = spv; } catch (err) {} try { localStorage.setItem(LAST_RATE_KEY, String(spv)); } catch (err) {} speedBtn.firstChild.nodeValue = `${spv}x`; speedBtn.removeAttribute('data-zero'); } highlightSelected(spv); hideSpeedMenu(); }); return opt; } let firstOption = true; for (const sp of SPEEDS) { menu.appendChild(makeOpt(sp, firstOption)); firstOption = false; } function highlightSelected(sp) { Array.from(menu.children).forEach(el => { el.style.background = 'none'; el.style.fontWeight = 'normal'; }); const sel = Array.from(menu.children).find(el => Number(el.dataset.sp) === Number(sp)); if (sel) { sel.style.background = 'rgba(255,255,255,0.14)'; sel.style.fontWeight = '700'; } } function showAndMeasure(el) { const originalDisplay = el.style.display; el.style.display = originalDisplay === 'none' ? 'block' : originalDisplay; el.style.visibility = 'hidden'; el.style.left = '-9999px'; el.style.top = '-9999px'; const rect = el.getBoundingClientRect(); el.style.display = 'none'; // hide it back return { w: rect.width, h: rect.height }; } function placeMenu() { const { w: menuW, h: menuH } = showAndMeasure(menu); const rect = speedBtn.getBoundingClientRect(); let left = Math.round(rect.left + rect.width / 2 - menuW / 2); if (left < EDGE) left = EDGE; if (left + menuW > window.innerWidth - EDGE) left = window.innerWidth - menuW - EDGE; let openAbove = rect.top - menuH - 6 >= EDGE; let top = openAbove ? Math.max(EDGE, rect.top - menuH - 6) : rect.bottom + 6; let openBelow = !openAbove; const items = Array.from(menu.children); const minSpeed = Math.min(...SPEEDS); const firstVal = items.length ? Number(items[0].dataset.sp) : NaN; if (openBelow) { if (firstVal === minSpeed) items.reverse().forEach(el => menu.appendChild(el)); } else { if (firstVal !== minSpeed) items.reverse().forEach(el => menu.appendChild(el)); } Object.assign(menu.style, { left: left + 'px', top: top + 'px', visibility: 'visible' }); } hideSpeedMenu = function() { menu.style.display = 'none'; if (skipMenu && skipMenu.style.display === 'none') { backdrop.style.display = 'none'; } }; function toggleSpeedMenu() { const open = menu.style.display === 'block'; hideSpeedMenu(); if (!open) { placeMenu(); menu.style.display = 'block'; backdrop.style.display = 'block'; if (activeVideo) highlightSelected(Number(activeVideo.playbackRate) || 1); } } // Create Skip Duration Menu skipMenu = document.createElement('div'); skipMenu.style.cssText = ` display: none; position: fixed; flex-direction: row; align-items: center; gap: 5px; padding: 5px; background: rgba(0,0,0,0.65); border-radius: 10px; z-index: 2147483647; pointer-events: auto; touch-action: manipulation; -webkit-tap-highlight-color: transparent; `; // --- Modified: Made buttons a little bigger --- const skipBtnStyle = ` appearance: none; border: 0; border-radius: 8px; padding: 6px 14px; font-size: 16px; font-weight: 600; color: #fff; background: rgba(70,70,70,0.5); line-height: 1.2; pointer-events: auto; user-select: none; `; SKIP_DURATIONS.forEach(duration => { const opt = document.createElement('button'); opt.textContent = `${duration}s`; opt.style.cssText = skipBtnStyle; opt.addEventListener('click', e => { e.stopPropagation(); if (activeVideo && longPressDirection !== 0) { activeVideo.currentTime = clampTime( activeVideo, activeVideo.currentTime + longPressDirection * duration ); } }); skipMenu.appendChild(opt); }); // --- New: Custom button to set main skip value --- const customSkipBtn = document.createElement('button'); customSkipBtn.textContent = '✎ Set'; customSkipBtn.title = 'Set default skip time'; customSkipBtn.style.cssText = skipBtnStyle; customSkipBtn.style.background = 'rgba(50, 80, 130, 0.6)'; // Different color to stand out customSkipBtn.addEventListener('click', e => { e.stopPropagation(); const choice = prompt("Set new default skip seconds:", skipSeconds); if (choice != null && choice !== "" && !isNaN(choice)) { saveSkip(Number(choice)); rewind.textContent = `⟲ ${skipSeconds}`; forward.textContent = `${skipSeconds} ⟳`; } hideSkipMenu(); // Close menu after setting }); skipMenu.appendChild(customSkipBtn); document.body.appendChild(skipMenu); function showSkipMenu() { const { w: menuW, h: menuH } = showAndMeasure(skipMenu); const rect = uiWrap.getBoundingClientRect(); // --- Modified: Default to above but closer, fallback to below --- let top = rect.top - menuH - 4; // 4px spacing above // Fallback to below if no space above if (top < EDGE) { top = rect.bottom + 8; } let left = Math.round(rect.left + rect.width / 2 - menuW / 2); left = clamp(left, EDGE, window.innerWidth - menuW - EDGE); top = clamp(top, EDGE, window.innerHeight - menuH - EDGE); Object.assign(skipMenu.style, { left: `${left}px`, top: `${top}px`, visibility: 'visible', display: 'flex' }); backdrop.style.display = 'block'; } // ---------- Skip handling ---------- function doSkip(dir) { if (activeVideo) { activeVideo.currentTime = clampTime(activeVideo, activeVideo.currentTime + dir * skipSeconds); } } rewind.onclick = e => { e.preventDefault(); doSkip(-1); }; forward.onclick = e => { e.preventDefault(); doSkip(1); }; const setupLongPress = (btn, dir) => { let pressTimer; btn.addEventListener("pointerdown", () => { pressTimer = setTimeout(() => { longPressDirection = dir; showSkipMenu(); }, 600); }); btn.addEventListener("pointerup", () => clearTimeout(pressTimer)); btn.addEventListener("pointerleave", () => clearTimeout(pressTimer)); btn.addEventListener("pointercancel", () => clearTimeout(pressTimer)); }; setupLongPress(rewind, -1); setupLongPress(forward, 1); // ---------- Speed handling (click menu OR vertical drag) ---------- let isSliding = false; let dragStartY = null, dragStartRate = null, moved = 0; speedBtn.addEventListener("pointerdown", e => { e.preventDefault(); dragStartY = e.clientY; dragStartRate = activeVideo ? activeVideo.playbackRate : 1; moved = 0; isSliding = false; try { speedBtn.setPointerCapture(e.pointerId); } catch (err) {} }); speedBtn.addEventListener("pointermove", e => { if (dragging || dragStartY == null || !activeVideo) return; // এই লাইনটি পরিবর্তন করা হয়েছে const dy = dragStartY - e.clientY; moved += Math.abs(e.movementY || (dragStartY - e.clientY)); if (moved > 6) isSliding = true; if (!isSliding) return; let newRate = dragStartRate + dy * 0.005; newRate = clamp(newRate, 0.1, 6); activeVideo.playbackRate = newRate; speedBtn.firstChild.nodeValue = newRate.toFixed(2) + "x"; try { localStorage.setItem(LAST_RATE_KEY, String(newRate)); } catch (err) {} speedBtn.removeAttribute('data-zero'); }); speedBtn.addEventListener("pointerup", e => { try { speedBtn.releasePointerCapture(e.pointerId); } catch (err) {} dragStartY = null; dragStartRate = null; if (isSliding) { isSliding = false; return; } }); speedBtn.addEventListener("click", e => { e.preventDefault(); if (speedBtn.getAttribute('data-zero') === '1') { if (!activeVideo) return; if (activeVideo.paused) { const last = Number(localStorage.getItem(LAST_RATE_KEY)) || 1; try { activeVideo.playbackRate = last; } catch (err) {} try { activeVideo.play(); } catch (err) {} speedBtn.firstChild.nodeValue = `${(last).toFixed(2)}x`.replace(/\.00$/,''); speedBtn.removeAttribute('data-zero'); } else { try { localStorage.setItem(LAST_RATE_KEY, String(activeVideo.playbackRate)); } catch (err) {} try { activeVideo.pause(); } catch (err) {} speedBtn.firstChild.nodeValue = 'Play'; } return; } toggleSpeedMenu(); }); row.append(rewind, speedBtn, forward); panel.append(dragHandle, row); uiWrap.append(panel); document.body.appendChild(uiWrap); // Dragging for the whole widget let dragging = false; dragHandle.onpointerdown = e => { dragging = true; try { dragHandle.setPointerCapture(e.pointerId); } catch (err) {} moveUnderFinger(e); manualDrag = true; e.preventDefault(); e.stopPropagation(); }; dragHandle.onpointermove = e => { if (!dragging) return; moveUnderFinger(e); e.preventDefault(); e.stopPropagation(); }; dragHandle.onpointerup = e => { dragging = false; }; dragHandle.onpointercancel = () => { dragging = false; }; function moveUnderFinger(e) { const w = uiWrap.offsetWidth, h = uiWrap.offsetHeight; const x = clamp(e.clientX - w / 2, EDGE, window.innerWidth - w - EDGE); const y = clamp(e.clientY - h / 2 - DRAG_OFFSET_Y, EDGE, window.innerHeight - h - EDGE); uiWrap.style.left = x + 'px'; uiWrap.style.top = y + 'px'; uiWrap.style.right = 'auto'; uiWrap.style.bottom = 'auto'; } // --- Fullscreen support: move UI/menu/backdrop into fullscreen container --- function getFullscreenElement() { return document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement || null; } function getFullscreenContainer(fsEl) { if (!fsEl) return null; try { if (fsEl.tagName && fsEl.tagName.toLowerCase() === 'video' && fsEl.parentElement) { return fsEl.parentElement; } if (fsEl.shadowRoot) return fsEl.shadowRoot; return fsEl; } catch (e) { return fsEl; } } function moveToContainer(container) { if (!container) return; try { const append = node => { if (!node) return; if (container instanceof ShadowRoot) container.appendChild(node); else if (container.appendChild) container.appendChild(node); }; append(uiWrap); append(menu); append(backdrop); append(skipMenu); } catch (e) {} } function enterFullscreenMode(fsEl) { if (!fsEl) return; const container = getFullscreenContainer(fsEl); if (!container) return; moveToContainer(container); uiWrap.style.position = 'absolute'; menu.style.position = 'absolute'; backdrop.style.position = 'absolute'; skipMenu.style.position = 'absolute'; uiWrap.style.zIndex = 2147483647; menu.style.zIndex = 2147483647; skipMenu.style.zIndex = 2147483647; backdrop.style.zIndex = 2147483646; positionOnVideo(); } function exitFullscreenMode() { try { document.body.appendChild(uiWrap); document.body.appendChild(menu); document.body.appendChild(backdrop); document.body.appendChild(skipMenu); } catch (e) {} uiWrap.style.position = 'fixed'; menu.style.position = 'fixed'; backdrop.style.position = 'fixed'; skipMenu.style.position = 'fixed'; uiWrap.style.zIndex = 2147483647; menu.style.zIndex = 2147483647; skipMenu.style.zIndex = 2147483647; backdrop.style.zIndex = 2147483646; positionOnVideo(); } function onFullScreenChange() { const fsEl = getFullscreenElement(); if (fsEl) enterFullscreenMode(fsEl); else exitFullscreenMode(); } document.addEventListener('fullscreenchange', onFullScreenChange); document.addEventListener('webkitfullscreenchange', onFullScreenChange); document.addEventListener('mozfullscreenchange', onFullScreenChange); document.addEventListener('MSFullscreenChange', onFullScreenChange); onFullScreenChange(); } // ---------- Video handling ---------- function visibleArea(v) { const r = v.getBoundingClientRect(); const iw = window.innerWidth, ih = window.innerHeight; const w = Math.max(0, Math.min(r.right, iw) - Math.max(r.left, 0)); const h = Math.max(0, Math.min(r.bottom, ih) - Math.max(r.top, 0)); return w * h; } const isPlaying = v => !v.paused && !v.ended && v.readyState > 2; function pickCandidate() { const vids = Array.from(document.querySelectorAll('video')); if (!vids.length) return null; let best = null, bestScore = -1, bestFrac = 0; const viewArea = window.innerWidth * window.innerHeight; for (const v of vids) { const area = visibleArea(v); if (area <= 0) continue; const frac = area / viewArea; const playing = isPlaying(v); const score = area + (playing ? viewArea : 0); if (score > bestScore) { best = v; bestScore = score; bestFrac = frac; } } if (!best) return null; const minFrac = isPlaying(best) ? MIN_FRAC_PLAYING : MIN_FRAC_PAUSED; if (bestFrac >= minFrac) return best; return null; } function setActiveVideo(v) { if (activeVideo === v) return; activeVideo = v; if (!activeVideo) { uiWrap.style.display = 'none'; return; } if (!document.body.contains(uiWrap)) document.body.appendChild(uiWrap); uiWrap.style.display = 'block'; manualDrag = false; hideSpeedMenu(); positionOnVideo(); try { if (speedBtn && speedBtn.getAttribute('data-zero') === '1') { speedBtn.textContent = activeVideo.paused ? 'Play' : 'Pause'; } else { speedBtn.textContent = (activeVideo && activeVideo.playbackRate) ? activeVideo.playbackRate.toFixed(2) + 'x' : '1x'; } } catch (e) {} } function positionOnVideo() { if (!activeVideo || manualDrag) return; const r = activeVideo.getBoundingClientRect(); const w = uiWrap.offsetWidth, h = uiWrap.offsetHeight; if (!(r && r.width > 0 && r.height > 0)) return; let x = r.right - w - 40; let y = r.bottom - h - 10; x = clamp(x, EDGE, window.innerWidth - w - EDGE); y = clamp(y, EDGE, window.innerHeight - h - EDGE); uiWrap.style.left = x + 'px'; uiWrap.style.top = y + 'px'; } function evaluateActive() { const cand = pickCandidate(); setActiveVideo(cand); } function tick() { try { positionOnVideo(); evaluateActive(); } catch (e) {} requestAnimationFrame(tick); } function observeVideos() { const io = new IntersectionObserver(() => evaluateActive(), { threshold: [0,0.25,0.5,0.75,1] }); const observed = new WeakSet(); const attachIO = () => { document.querySelectorAll('video').forEach(v => { if (!observed.has(v)) { io.observe(v); observed.add(v); } }); }; attachIO(); new MutationObserver(attachIO).observe(document.body, { childList: true, subtree: true }); setInterval(evaluateActive, 1500); addEventListener('resize', positionOnVideo); addEventListener('scroll', () => { evaluateActive(); positionOnVideo(); }, { passive: true }); } function init() { createUI(); observeVideos(); evaluateActive(); positionOnVideo(); requestAnimationFrame(tick); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true }); else init(); })();