(function() { const tpl_player = (svg, size) => `
Background
Autonext
Danmaku
2X Speed
`; const isMobile = /Mobi/i.test(navigator.userAgent) || ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); let mouseX = -1, mouseY = -1; const updateHoverStates = () => { // Block interaction if content warning is visible or on mobile const cwModal = document.getElementById('content-warning-modal'); if ((cwModal && cwModal.style.display !== 'none') || isMobile) return; document.querySelectorAll('.v0ck').forEach(p => { const rect = p.getBoundingClientRect(); const isOver = mouseX >= rect.left && mouseX <= rect.right && mouseY >= rect.top && mouseY <= rect.bottom; p.classList.toggle("v0ck_hover", isOver); }); }; document.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; updateHoverStates(); }, { passive: true }); window.addEventListener('scroll', updateHoverStates, { passive: true }); window.addEventListener('wheel', updateHoverStates, { passive: true }); class v0ck { constructor(elem) { const tagName = elem.tagName.toLowerCase(); if (["video", "audio"].includes(tagName)) { const parent = elem.parentElement; if (parent.querySelector('.v0ck_player_controls')) { window.f0ckDebug("[v0ck] Player controls already exist, skipping injection and init"); return elem; // Return the video element as the constructor result } else { parent.classList.add("v0ck", "paused"); elem.classList.add("v0ck_video", "viewer"); // Check if mouse is already inside the element synchronously to avoid transition flicker const rect = parent.getBoundingClientRect(); if (mouseX >= rect.left && mouseX <= rect.right && mouseY >= rect.top && mouseY <= rect.bottom) { parent.classList.add("v0ck_hover", "v0ck_no_transition"); // Remove no-transition after a frame setTimeout(() => parent.classList.remove("v0ck_no_transition"), 50); } if (!document.querySelector('link[href^="/s/css/v0ck.css"]')) { document.head.insertAdjacentHTML("beforeend", ``); // inject css } // Use absolute path for reliable asset loading const size = elem.getAttribute('data-size'); elem.insertAdjacentHTML("afterend", tpl_player(`/s/img/v0ck.svg`, size)); window.f0ckDebug("[v0ck] Player initialized for", tagName); } if (tagName === "audio" && elem.hasAttribute('poster')) { // set cover const player = document.querySelector('.v0ck'); player.style.backgroundImage = `url('${elem.getAttribute('poster')}')`; } } else return console.error("nope"); return this.init(elem); } init(elem) { const player = document.querySelector('.v0ck'); const video = elem; video.removeAttribute('controls'); video.removeAttribute('autoplay'); video.addEventListener('contextmenu', e => { if (isMobile) e.preventDefault(); // Block native download/options menu on mobile only }); const progress = player.querySelector('.v0ck_progress'); const progressBar = player.querySelector('.v0ck_progress_filled'); const bufferBar = player.querySelector('.v0ck_progress_buffered'); const seekMarker = player.querySelector('.v0ck_seek_marker'); const loader = player.querySelector('.v0ck_loader'); const toggle = player.querySelector('.v0ck_toggle'); const skipButtons = player.querySelectorAll('.v0ck [data-skip]'); const ranges = player.querySelectorAll('.v0ck input[type="range"]'); const volumeSlider = player.querySelector('.v0ck input[type="range"][name="volume"]'); const fullscreen = player.querySelector('.v0ck_fs_btn'); const playtime = player.querySelector('.v0ck_playtime'); const overlay = player.querySelector('.v0ck_overlay'); const volumeButton = player.querySelector('.v0ck_volume'); const volumeSymbols = volumeButton.querySelectorAll('use'); const defaultVolume = 0.5; let mousedown = false; let _volume; // Hold to speedup (2x) states let speedUpTimeout; let isSpeedingUp = false; let restorePlaybackRate = 1; let ignoreNextClick = false; let wasPausedWhenStarted = false; const speedIndicator = player.querySelector('.v0ck_speed_indicator'); // (mouse position is now tracked via docMouseX/docMouseY in resetControlsTimer block) function handleVolumeButton(vol) { [...volumeSymbols].forEach(s => s.classList.add('v0ck_hidden')); let targetId = 'v0ck_svg_volume_full'; if (vol === 0) { targetId = 'v0ck_svg_volume_mute'; } else if (vol <= 0.5) { targetId = 'v0ck_svg_volume_mid'; } const activeSymbol = [...volumeSymbols].find(s => s.id === targetId); if (activeSymbol) { activeSymbol.classList.remove('v0ck_hidden'); } localStorage.setItem("volume", vol); } function togglePlay() { return video[video.paused ? 'play' : 'pause'](); } function updatePlayIcon() { toggle.classList.toggle('playing'); player.classList.toggle('paused'); toggle.setAttribute('title', toggle.classList.contains('playing') ? 'Pause' : 'Play'); [...toggle.querySelectorAll('use')].forEach(icon => icon.classList.toggle('v0ck_hidden')); } function toggleMute(e) { if (video.volume === 0) video.volume = volumeSlider.value = _volume === 0 ? defaultVolume : _volume; else { _volume = video.volume; video.volume = volumeSlider.value = 0; } handleVolumeButton(video.volume); } function skip() { video.currentTime += +this.dataset.skip; } function handleRangeUpdate() { video[this.name] = this.value; _volume = video.volume; handleVolumeButton(video.volume); } function formatTime(seconds) { const minutes = (~~(seconds / 60)).toString().padStart(2, "0"); seconds = (~~(seconds % 60)).toString().padStart(2, "0"); return minutes + ":" + seconds; } function handleProgress() { const percent = (video.currentTime / video.duration) * 100; progressBar.style.flexBasis = percent + '%'; playtime.innerText = `${formatTime(video.currentTime)} / ${formatTime(video.duration)}`; if (video.buffered.length > 0) { const bufferedEnd = video.buffered.end(video.buffered.length - 1); const duration = video.duration; if (duration > 0) { bufferBar.style.width = (bufferedEnd / duration) * 100 + '%'; } } } function scrub(e) { let x; if (e.type.startsWith('touch')) { const rect = progress.getBoundingClientRect(); x = e.touches[0].clientX - rect.left; if (e.type === 'touchmove' && e.cancelable) e.preventDefault(); } else { x = e.offsetX; } const width = progress.offsetWidth; // Clamp x between 0 and width x = Math.max(0, Math.min(x, width)); const scrubTime = (x / width) * video.duration; if (!Number.isFinite(scrubTime)) return; video.currentTime = scrubTime; // Visual seek marker seekMarker.style.left = `${(x / width) * 100}%`; seekMarker.classList.remove('active'); void seekMarker.offsetWidth; // trigger reflow seekMarker.classList.add('active'); } function enterFullScreen() { if (document.fullscreenElement) return; const target = document.getElementById('main') || player; if (/(iPad|iPhone|iPod)/gi.test(navigator.platform)) video.webkitEnterFullscreen(); else target.requestFullscreen(); } function toggleFullScreen(e) { if (document.fullscreenElement) // exit fullscreen document.exitFullscreen(); else { // request fullscreen enterFullScreen(); } } function toggleFullScreenClasses() { const fsElem = document.fullscreenElement; const isThisPlayerFS = fsElem && (fsElem === player || fsElem.contains(player)); player.classList.toggle('v0ck_fullscreen', !!isThisPlayerFS); } player.addEventListener('click', e => { if (ignoreNextClick) { e.stopPropagation(); e.preventDefault(); ignoreNextClick = false; return; } const path = e.path || (e.composedPath && e.composedPath()); const isControls = !!path.filter(f => f.classList?.contains('v0ck_player_controls')).length; if (!isControls) { if (isMobile && !player.classList.contains('v0ck_hover')) { player.classList.add('v0ck_hover'); return; } togglePlay(e); } }); toggle.addEventListener('click', togglePlay); overlay.addEventListener('click', e => { e.stopPropagation(); player.classList.add('v0ck_hover'); togglePlay(); }); video.addEventListener('play', updatePlayIcon); // Robust initial overlay removal const removeInitial = () => player.classList.remove('v0ck_initial'); video.addEventListener('play', removeInitial); video.addEventListener('playing', removeInitial); video.addEventListener('timeupdate', () => { if (video.currentTime > 0.1 && !video.paused && !video.ended) removeInitial(); }); video.addEventListener('pause', updatePlayIcon); video.addEventListener('timeupdate', handleProgress); video.addEventListener('progress', handleProgress); video.addEventListener('ended', () => { if (localStorage.getItem('autoplay') === 'true') { const nextBtn = document.getElementById('next'); if (nextBtn && nextBtn.href && !nextBtn.href.endsWith('#')) { nextBtn.click(); } } }); // Loader events const showLoader = () => loader.classList.remove('v0ck_hidden'); const hideLoader = () => loader.classList.add('v0ck_hidden'); video.addEventListener('waiting', showLoader); video.addEventListener('stalled', showLoader); video.addEventListener('canplay', hideLoader); video.addEventListener('playing', hideLoader); video.addEventListener('seeked', hideLoader); video.addEventListener('loadeddata', hideLoader); volumeButton.addEventListener('click', e => { e.stopPropagation(); toggleMute(e); }); const hud = player.querySelector('.v0ck_hud'); const hudBar = hud.querySelector('.v0ck_hud_bar'); const hudIcon = hud.querySelector('.v0ck_hud_icon'); let startX, startY, startVol, isRightSide, gestureType; let hudTimer; function showHUD(vol) { hud.classList.remove('v0ck_hidden'); hudBar.style.width = `${vol * 100}%`; // Update HUD icon based on volume let icon = 'volume_full'; if (vol === 0) icon = 'volume_mute'; else if (vol <= 0.5) icon = 'volume_mid'; const baseSvg = (hudIcon.getAttribute('href') || hudIcon.getAttribute('xlink:href') || '/s/img/v0ck.svg').split('#')[0]; hudIcon.setAttribute('href', `${baseSvg}#${icon}`); hudIcon.setAttribute('xlink:href', `${baseSvg}#${icon}`); clearTimeout(hudTimer); hudTimer = setTimeout(() => hud.classList.add('v0ck_hidden'), 1000); } player.addEventListener('touchstart', e => { if (!isMobile) return; const touch = e.touches[0]; const rect = player.getBoundingClientRect(); const x = touch.clientX - rect.left; isRightSide = x > rect.width / 2; gestureType = 'none'; if (isRightSide) { startX = touch.clientX; startY = touch.clientY; startVol = video.volume; } }, { passive: false }); player.addEventListener('touchmove', e => { if (!isMobile || !isRightSide || gestureType === 'other') return; const touch = e.touches[0]; const dx = Math.abs(touch.clientX - startX); const dy = Math.abs(touch.clientY - startY); // Identify gesture type if not yet locked if (gestureType === 'none') { if (dy > dx && dy > 5) { gestureType = 'volume'; clearTimeout(speedUpTimeout); endSpeedUp(); } else if (dx > dy && dx > 5) { gestureType = 'other'; // Probably seeking or horizontal swipe return; } else { return; // Too small movement to decide } } if (gestureType === 'volume') { clearTimeout(speedUpTimeout); endSpeedUp(); const deltaY = startY - touch.clientY; // swipe up is positive const sensitivity = 200; // pixels for 0 to 1 range (reverted to original) let newVol = startVol + (deltaY / sensitivity); newVol = Math.max(0, Math.min(1, newVol)); video.volume = newVol; // Set directly for smoothness volumeSlider.value = newVol; // Update visual slider _volume = newVol; handleVolumeButton(newVol); showHUD(newVol); if (e.cancelable) e.preventDefault(); } }, { passive: false }); // Desktop mouse volume gesture support (clicking and dragging vertically on the right half of the player) let activeMouseGesture = false; player.addEventListener('mousedown', e => { if (isMobile) return; if (e.button !== 0) return; const path = e.path || (e.composedPath && e.composedPath()); const isControls = !!path.filter(f => f.classList?.contains('v0ck_player_controls')).length; if (isControls) return; const rect = player.getBoundingClientRect(); const x = e.clientX - rect.left; isRightSide = x > rect.width / 2; gestureType = 'none'; if (isRightSide) { startX = e.clientX; startY = e.clientY; startVol = video.volume; activeMouseGesture = true; } }); window.addEventListener('mousemove', e => { if (!activeMouseGesture || gestureType === 'other') return; const dx = Math.abs(e.clientX - startX); const dy = Math.abs(e.clientY - startY); if (gestureType === 'none') { if (dy > dx && dy > 5) { gestureType = 'volume'; clearTimeout(speedUpTimeout); endSpeedUp(); } else if (dx > dy && dx > 5) { gestureType = 'other'; return; } else { return; } } if (gestureType === 'volume') { clearTimeout(speedUpTimeout); endSpeedUp(); ignoreNextClick = true; const deltaY = startY - e.clientY; // swipe up is positive const sensitivity = 200; let newVol = startVol + (deltaY / sensitivity); newVol = Math.max(0, Math.min(1, newVol)); video.volume = newVol; volumeSlider.value = newVol; _volume = newVol; handleVolumeButton(newVol); showHUD(newVol); e.preventDefault(); } }); window.addEventListener('mouseup', () => { if (activeMouseGesture) { activeMouseGesture = false; setTimeout(() => { ignoreNextClick = false; }, 100); } }); skipButtons.forEach(button => button.addEventListener('click', skip)); ranges.forEach(range => range.addEventListener('change', handleRangeUpdate)); ranges.forEach(range => range.addEventListener('input', handleRangeUpdate)); ranges.forEach(range => range.addEventListener('mousemove', handleRangeUpdate)); // Prevent touch events on the volume slider from bubbling to the player container (avoiding gesture conflicts and page scrolls) if (volumeSlider) { ['touchstart', 'touchmove', 'touchend', 'touchcancel'].forEach(evt => { volumeSlider.addEventListener(evt, e => { e.stopPropagation(); }, { passive: false }); }); } progress.addEventListener('mousedown', scrub); progress.addEventListener('touchstart', scrub, { passive: false }); progress.addEventListener('touchmove', scrub, { passive: false }); fullscreen.addEventListener('click', toggleFullScreen); document.addEventListener('fullscreenchange', toggleFullScreenClasses); toggleFullScreenClasses(); // Check initial state (important for transitions) video.volume = _volume = volumeSlider.value = +(localStorage.getItem('volume') ?? defaultVolume); handleVolumeButton(video.volume); const mediaObj = player.closest('.media-object'); let isBlurredDetail = false; if (mediaObj && localStorage.getItem('blurDetail') !== 'false') { const mode = mediaObj.getAttribute('data-mode'); const blurNsfw = localStorage.getItem('blurNsfw') === 'true'; const blurNsfl = localStorage.getItem('blurNsfl') === 'true'; const blurSfw = localStorage.getItem('blurSfw') === 'true'; const blurUntagged = localStorage.getItem('blurUntagged') === 'true'; let shouldBlurThis = false; if (mode === 'nsfw') shouldBlurThis = blurNsfw; else if (mode === 'nsfl') shouldBlurThis = blurNsfl; else if (mode === 'sfw') shouldBlurThis = blurSfw; else if (mode === 'untagged') shouldBlurThis = blurUntagged; if (shouldBlurThis && !mediaObj.classList.contains('revealed')) { isBlurredDetail = true; } } // Attempt autoplay and show overlay if blocked const shouldAutoplay = !isBlurredDetail && window.f0ckSession?.disable_autoplay !== true; if (shouldAutoplay) { const playPromise = togglePlay(); if (playPromise !== undefined) { playPromise.catch(() => { player.classList.add('v0ck_initial'); }); } else if (video.paused) { player.classList.add('v0ck_initial'); } } else { player.classList.add('v0ck_initial'); } // Settings Menu Logic const settingsBtn = player.querySelector('.v0ck_settings_btn'); const settingsMenu = player.querySelector('.v0ck_settings_menu'); const toggleBgSwitch = player.querySelector('#togglebg'); const downloadBtn = player.querySelector('#v0ck_download'); if (downloadBtn) { downloadBtn.addEventListener('click', (e) => { e.stopPropagation(); const a = document.createElement('a'); a.href = video.src; a.download = video.src.split('/').pop(); document.body.appendChild(a); a.click(); document.body.removeChild(a); }); } // Initialize switch state const bgEnabled = (window.f0ckSession && window.f0ckSession.show_background !== undefined) ? !!window.f0ckSession.show_background : (localStorage.getItem('background') !== 'false'); if (toggleBgSwitch && bgEnabled) { toggleBgSwitch.classList.add('active'); } const toggleAutoplaySwitch = player.querySelector('#toggleautoplay'); if (toggleAutoplaySwitch && localStorage.getItem('autoplay') === 'true') { toggleAutoplaySwitch.classList.add('active'); video.loop = false; } if(settingsBtn && settingsMenu) { settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); settingsMenu.classList.toggle('v0ck_hidden'); if (settingsMenu.classList.contains('v0ck_hidden')) { document.dispatchEvent(new CustomEvent('v0ck_settings_closed')); } }); // Close menu/panel when clicking outside document.addEventListener('click', (e) => { const isFlashYankUI = e.target.closest('#flash-yank-ui'); const isInsidePlayer = player.contains(e.target); if(!settingsMenu.contains(e.target) && !settingsBtn.contains(e.target) && !isFlashYankUI) { if (!settingsMenu.classList.contains('v0ck_hidden')) { settingsMenu.classList.add('v0ck_hidden'); document.dispatchEvent(new CustomEvent('v0ck_settings_closed')); } } if (isMobile && !isInsidePlayer && !isFlashYankUI) { player.classList.remove('v0ck_hover'); } }); // Prevent menu click from pausing video (only if clicked on non-button area) settingsMenu.addEventListener('click', (e) => { if (!e.target.closest('button') && !e.target.closest('.v0ck_bg_row')) { e.stopPropagation(); } }); // Visual toggle for background switch if (toggleBgSwitch) { const bgRow = toggleBgSwitch.closest('.v0ck_bg_row'); const handleBgToggle = (e) => { e.stopPropagation(); if (window.toggleBackground) { window.toggleBackground(); } else { toggleBgSwitch.classList.toggle('active'); } }; if (bgRow) bgRow.addEventListener('click', handleBgToggle); else toggleBgSwitch.addEventListener('click', handleBgToggle); } if (toggleAutoplaySwitch) { const autoplayRow = toggleAutoplaySwitch.closest('.v0ck_bg_row'); const handleAutoplayToggle = (e) => { e.stopPropagation(); if (window.toggleAutoplay) { window.toggleAutoplay(); } else { toggleAutoplaySwitch.classList.toggle('active'); } video.loop = !toggleAutoplaySwitch.classList.contains('active'); }; if (autoplayRow) autoplayRow.addEventListener('click', handleAutoplayToggle); else toggleAutoplaySwitch.addEventListener('click', handleAutoplayToggle); } // Danmaku toggle const toggleDanmakuSwitch = player.querySelector('#toggledanmaku'); if (toggleDanmakuSwitch) { // Sync initial state from localStorage vs site-wide config default const configDefault = (window.f0ckSession && window.f0ckSession.enable_danmaku !== undefined) ? !!window.f0ckSession.enable_danmaku : true; const savedPref = localStorage.getItem('danmaku'); const isEnabled = (savedPref !== null) ? (savedPref !== 'false') : configDefault; if (isEnabled) { toggleDanmakuSwitch.classList.add('active'); } const danmakuRow = toggleDanmakuSwitch.closest('.v0ck_bg_row'); const handleDanmakuToggle = (e) => { e.stopPropagation(); if (window.danmakuInstance) { window.danmakuInstance.toggle(); const on = window.danmakuInstance.isEnabled(); toggleDanmakuSwitch.classList.toggle('active', on); if (window.flashMessage) window.flashMessage(on ? 'Danmaku ON' : 'Danmaku OFF', 1800); } else { toggleDanmakuSwitch.classList.toggle('active'); const newVal = toggleDanmakuSwitch.classList.contains('active'); localStorage.setItem('danmaku', newVal ? 'true' : 'false'); if (window.flashMessage) window.flashMessage(newVal ? 'Danmaku ON' : 'Danmaku OFF', 1800); } }; if (danmakuRow) danmakuRow.addEventListener('click', handleDanmakuToggle); else toggleDanmakuSwitch.addEventListener('click', handleDanmakuToggle); } } // Controls auto-hide logic (auto hide controls after 2.5 seconds of inactivity) let controlsTimer; // Track real mouse position at document level — completely independent of // any element animation or synthetic events. let docMouseX = -1; let docMouseY = -1; const onDocMouseMove = (e) => { docMouseX = e.clientX; docMouseY = e.clientY; }; document.addEventListener('mousemove', onDocMouseMove, { passive: true }); function resetControlsTimer() { clearTimeout(controlsTimer); const isFullscreen = player.classList.contains('v0ck_fullscreen'); if (!video.paused || isFullscreen) { controlsTimer = setTimeout(() => { player.classList.remove('v0ck_hover'); if (settingsMenu && !settingsMenu.classList.contains('v0ck_hidden')) { settingsMenu.classList.add('v0ck_hidden'); document.dispatchEvent(new CustomEvent('v0ck_settings_closed')); } }, 2500); } } function showControlsAndReset(e) { // Ignore synthetic pointer events fired by the browser during CSS animations // (element shifts under stationary cursor). We compare against docMouseX/Y // which is only ever updated by genuine user mouse movement. if (e && e.clientX !== undefined && e.clientY !== undefined) { if (e.clientX === docMouseX && e.clientY === docMouseY && player.classList.contains('v0ck_hover')) { // Coordinates unchanged and controls already visible — synthetic event, skip. return; } docMouseX = e.clientX; docMouseY = e.clientY; } player.classList.add('v0ck_hover'); resetControlsTimer(); } // Events that should show controls and reset/extend the auto-hide timer const resetEvents = ['touchstart', 'touchmove', 'touchend', 'click', 'mousemove', 'mouseenter']; resetEvents.forEach(evt => { player.addEventListener(evt, showControlsAndReset, { capture: true, passive: true }); }); // Hide when cursor leaves the player entirely. // NO capture:true — that would incorrectly intercept mouseleave events from // child elements (e.g. the progress bar animating away from the cursor). // Without capture, this only fires when the mouse truly leaves .v0ck itself. player.addEventListener('mouseleave', (e) => { const r = player.getBoundingClientRect(); // Grace zone: the controls bar peeks ~3px below the player when hidden. // If the cursor is still in that strip, don't hide. if (e.clientX >= r.left && e.clientX <= r.right && e.clientY > r.bottom && e.clientY <= r.bottom + 8) { return; } docMouseX = -1; docMouseY = -1; player.classList.remove('v0ck_hover'); clearTimeout(controlsTimer); }); video.addEventListener('play', resetControlsTimer); video.addEventListener('playing', resetControlsTimer); video.addEventListener('pause', () => clearTimeout(controlsTimer)); // Speedup 2x on Hold logic function startSpeedUp(e) { if (e.type === 'mousedown' && isMobile) return; // Only left mouse click or touch triggers speedup if (e.type === 'mousedown' && e.button !== 0) return; // Don't speed up if clicking on controls or settings panel const path = e.path || (e.composedPath && e.composedPath()); const isControls = !!path.filter(f => f.classList?.contains('v0ck_player_controls')).length; if (isControls) return; clearTimeout(speedUpTimeout); speedUpTimeout = setTimeout(() => { isSpeedingUp = true; ignoreNextClick = true; wasPausedWhenStarted = video.paused; restorePlaybackRate = video.playbackRate; video.playbackRate = 2.0; if (wasPausedWhenStarted) { video.play(); } if (speedIndicator) { speedIndicator.classList.remove('v0ck_hidden'); } }, 500); } function endSpeedUp(e) { clearTimeout(speedUpTimeout); if (isSpeedingUp) { isSpeedingUp = false; video.playbackRate = restorePlaybackRate; if (wasPausedWhenStarted) { video.pause(); wasPausedWhenStarted = false; } if (speedIndicator) { speedIndicator.classList.add('v0ck_hidden'); } // Brief timeout before allowing normal clicking again to bypass the immediate click event setTimeout(() => { ignoreNextClick = false; }, 100); } } player.addEventListener('mousedown', startSpeedUp); player.addEventListener('touchstart', startSpeedUp, { passive: true }); player.addEventListener('mouseup', endSpeedUp); player.addEventListener('mouseleave', endSpeedUp); player.addEventListener('touchend', endSpeedUp); player.addEventListener('touchcancel', endSpeedUp); this.toggleFullScreen = toggleFullScreen; this.enterFullScreen = enterFullScreen; return video; } } window.v0ck = v0ck; })();