(function () { 'use strict'; const STORAGE_KEY = 'w0bmFlashFilterSettings_v1'; const DEFAULT_SETTINGS = { yank: 60, // 0..100 master "yank" enabled: false, advanced: false, // if true, use individual params instead of yank mapping enableResolution: true, // per-axis degradation toggles (advanced only) enableFps: true, enablePalette: true, internalWidth: 320, fps: 12, paletteLevels: 4 }; let settings = loadSettings(); let currentConfig = computeConfig(); let chanLUT = buildChannelLUT(currentConfig.paletteLevels); let currentController = null; let hotkeyAttached = false; let ui = null; // { panel, slider, yankValue, info, toggle } const isMobile = /Mobi/i.test(navigator.userAgent); function isItemPage() { const path = window.location.pathname; // Strictly match item pages (e.g., /123, /user/name/123) and exclude grids/specials const isItem = (path.match(/^\/\d+/) || path.split('/').some(s => /^\d+$/.test(s))) && !path.match(/\/p\//); const isForbidden = path === '/upload' || path.startsWith('/admin') || path.startsWith('/mod'); return isItem && !isForbidden; } // ---------- Settings / Config ---------- function loadSettings() { try { const val = localStorage.getItem(STORAGE_KEY); if (val) { const obj = JSON.parse(val); return Object.assign({}, DEFAULT_SETTINGS, obj); } } catch (e) { console.error('Failed to load settings from localStorage', e); } return Object.assign({}, DEFAULT_SETTINGS); } function saveSettings() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } catch (e) { console.error('Failed to save settings to localStorage', e); } } // Yank 0..100 => config // Higher yank = harsher: lower res, lower FPS, fewer colors function computeConfigFromYank(yank) { const t = Math.min(1, Math.max(0, yank / 100)); // Match the ranges used by the advanced sliders: // Resolution: 160..800, FPS: 8..30, Palette levels: 2..12 const widthMax = 800, widthMin = 160; const fpsMax = 30, fpsMin = 8; const levelsMax = 12, levelsMin = 2; // per channel const width = Math.round(widthMax - (widthMax - widthMin) * t); const height = Math.round(width * 9 / 16); const fps = Math.round(fpsMax - (fpsMax - fpsMin) * t); const paletteLevels = Math.round(levelsMax - (levelsMax - levelsMin) * t); return { internalWidth: width, internalHeight: null, // Will be calculated dynamically based on aspect ratio fps, paletteLevels }; } // Update advanced parameters from current yank value function updateAdvancedFromYank() { const cfg = computeConfigFromYank(settings.yank); settings.internalWidth = cfg.internalWidth; settings.fps = cfg.fps; settings.paletteLevels = cfg.paletteLevels; } // Derive an approximate yank from current advanced sliders (0..100) function updateYankFromAdvanced() { const widthMax = 800, widthMin = 160; const fpsMax = 30, fpsMin = 8; const levelsMax = 12, levelsMin = 2; const tWidth = (widthMax - settings.internalWidth) / (widthMax - widthMin); const tFps = (fpsMax - settings.fps) / (fpsMax - fpsMin); const tPalette = (levelsMax - settings.paletteLevels) / (levelsMax - levelsMin); let t = (tWidth + tFps + tPalette) / 3; t = Math.min(1, Math.max(0, t)); settings.yank = Math.round(t * 100); } // Combine yank mapping with optional advanced overrides function computeConfig() { if (settings.advanced) { const w = clamp(settings.internalWidth || DEFAULT_SETTINGS.internalWidth, 160, 800); const fps = clamp(settings.fps || DEFAULT_SETTINGS.fps, 8, 30); const pl = clamp(settings.paletteLevels || DEFAULT_SETTINGS.paletteLevels, 2, 12); settings.internalWidth = w; settings.fps = fps; settings.paletteLevels = pl; return { internalWidth: w, internalHeight: null, // Calculated dynamically fps, paletteLevels: pl }; } const cfg = computeConfigFromYank(settings.yank); // keep advanced values in sync so toggling advanced inherits current feel settings.internalWidth = cfg.internalWidth; settings.fps = cfg.fps; settings.paletteLevels = cfg.paletteLevels; return cfg; } function clamp(v, min, max) { v = Number(v); if (isNaN(v)) return min; return Math.min(max, Math.max(min, v)); } function applySettingsToRuntime() { currentConfig = computeConfig(); chanLUT = buildChannelLUT(currentConfig.paletteLevels); if (currentController && currentController.onConfigChanged) { currentController.onConfigChanged(); } updateUIFromSettings(); } // ---------- Palette / Image processing ---------- function buildChannelLUT(levels) { const lut = new Uint8Array(256); if (levels <= 1) { lut.fill(0); return lut; } const step = 255 / (levels - 1); for (let i = 0; i < 256; i++) { const idx = Math.round(i / step); lut[i] = Math.round(idx * step); } return lut; } function applyPalette(imageData, lut) { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { data[i] = lut[data[i]]; data[i + 1] = lut[data[i + 1]]; data[i + 2] = lut[data[i + 2]]; // alpha unchanged } } // ---------- Video controller ---------- function createCanvasForVideo(video) { // Ensure parent is relative for absolute positioning of canvas const style = window.getComputedStyle(video.parentNode); if (style.position === 'static') { video.parentNode.style.position = 'relative'; } const canvas = document.createElement('canvas'); canvas.style.display = 'block'; canvas.style.position = 'absolute'; canvas.style.top = '0'; canvas.style.left = '0'; canvas.style.zIndex = '1'; // Lower z-index so controls can sit on top canvas.style.pointerEvents = 'none'; // Let clicks pass through to controls canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.backgroundColor = 'transparent'; canvas.style.imageRendering = 'pixelated'; canvas.style.objectFit = 'contain'; video.parentNode.insertBefore(canvas, video.nextSibling); return canvas; } function makeController(video) { const canvas = createCanvasForVideo(video); const ctx = canvas.getContext('2d', { willReadFrequently: true }); let enabled = false; let intervalId = null; function applyConfigToCanvas() { const rect = video.getBoundingClientRect(); const displayWidth = rect.width || video.clientWidth || video.width || 640; const displayHeight = rect.height || video.clientHeight || video.height || 360; let internalWidth = currentConfig.internalWidth; let internalHeight; // Calculate aspect ratio const videoWidth = video.videoWidth || 640; const videoHeight = video.videoHeight || 360; const aspectRatio = videoHeight / videoWidth; // If advanced resolution scaling is disabled, match display size for best quality if (settings.advanced && !settings.enableResolution) { internalWidth = Math.round(displayWidth); internalHeight = Math.round(displayHeight); } else { // Calculate height based on fixed width and aspect ratio internalHeight = Math.round(internalWidth * aspectRatio); } canvas.width = internalWidth; canvas.height = internalHeight; canvas.width = internalWidth; canvas.height = internalHeight; // The canvas fills the container (which is sized by the hidden video) canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.maxWidth = ''; canvas.style.aspectRatio = ''; } applyConfigToCanvas(); function drawFrame() { if (!enabled || video.paused || video.ended) return; try { const w = canvas.width; const h = canvas.height; ctx.drawImage(video, 0, 0, w, h); // If advanced palette reduction is disabled, skip quantization if (!(settings.advanced && !settings.enablePalette)) { const frame = ctx.getImageData(0, 0, w, h); applyPalette(frame, chanLUT); ctx.putImageData(frame, 0, 0); } } catch (e) { // ignore } } function startLoop() { if (intervalId || !enabled) return; const effectiveFps = (settings.advanced && !settings.enableFps) ? 60 : currentConfig.fps; intervalId = setInterval(drawFrame, 1000 / effectiveFps); } function stopLoop() { if (intervalId) { clearInterval(intervalId); intervalId = null; } } function enable() { if (enabled) return; enabled = true; canvas.style.display = 'block'; video.style.visibility = 'hidden'; // Keep layout space! if (!video.paused && !video.ended) startLoop(); } function disable() { if (!enabled) return; enabled = false; canvas.style.display = 'none'; video.style.visibility = ''; stopLoop(); } function toggle() { if (enabled) disable(); else enable(); } function isEnabled() { return enabled; } function onConfigChanged() { const wasEnabled = enabled; stopLoop(); applyConfigToCanvas(); if (wasEnabled) { startLoop(); } } function destroy() { disable(); canvas.remove(); video.removeEventListener('play', startLoop); video.removeEventListener('pause', stopLoop); video.removeEventListener('ended', stopLoop); } video.addEventListener('play', startLoop); video.addEventListener('pause', stopLoop); video.addEventListener('ended', stopLoop); video.addEventListener('loadedmetadata', applyConfigToCanvas); // Recalculate when metadata allows return { enable, disable, toggle, isEnabled, destroy, onConfigChanged }; } function setupVideo(video) { if (!video) return; if (!isItemPage()) { if (ui) ui.wrapper.style.display = 'none'; return; } // Prioritize the main item player (id="my-video" or class "viewer") const isPrimary = video.id === 'my-video' || video.classList.contains('viewer') || video.classList.contains('v0ck_video'); if (video.dataset.flashFilterAttached === '1') { // If already attached, ensure its currentController is restored if it's the primary one if (isPrimary && !currentController) { currentController = video.__flashFilterController; } return; } video.dataset.flashFilterAttached = '1'; const controller = makeController(video); video.__flashFilterController = controller; // If this is the primary video, or we don't have one yet, set as current if (isPrimary || !currentController) { currentController = controller; } // Always sync with global setting on creation if (settings.enabled) { controller.enable(); } else { controller.disable(); } // Small delay to ensure v0ck has injected its controls setTimeout(() => { if (ui) updateUIFromSettings(); }, 50); if (ui && isPrimary) { ui.wrapper.style.display = 'block'; } } function scanExistingVideos() { document.querySelectorAll('video').forEach(setupVideo); } function startObserver() { if (!document.body) return; const observer = new MutationObserver((mutations) => { for (const m of mutations) { m.addedNodes.forEach((node) => { if (node.nodeType !== 1) return; if (node.tagName === 'VIDEO') { setupVideo(node); } else if (node.querySelectorAll) { node.querySelectorAll('video').forEach(setupVideo); } }); m.removedNodes.forEach((node) => { if (node.nodeType !== 1) return; const vids = []; if (node.tagName === 'VIDEO') { vids.push(node); } if (node.querySelectorAll) { node.querySelectorAll('video').forEach(v => vids.push(v)); } vids.forEach((v) => { const c = v.__flashFilterController; if (c) { c.destroy(); if (currentController === c) { currentController = null; } } }); if (ui && document.querySelectorAll('video').length === 0) { ui.wrapper.style.display = 'none'; } }); } }); observer.observe(document.body, { childList: true, subtree: true }); } // ---------- UI Panel ---------- function createOptionsUI() { if (ui || !document.body) return; const wrapper = document.createElement('div'); wrapper.style.position = 'fixed'; wrapper.style.bottom = '10px'; wrapper.style.right = '10px'; wrapper.style.zIndex = '99999'; wrapper.style.fontFamily = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; wrapper.style.fontSize = '12px'; wrapper.style.color = '#fff'; wrapper.style.display = 'none'; // Initially hidden wrapper.id = 'flash-yank-ui'; const floatingBadge = document.createElement('div'); floatingBadge.id = 'w0bm-floating-swf'; floatingBadge.textContent = 'SWF'; floatingBadge.style.background = 'rgba(0,0,0,0.9)'; floatingBadge.style.padding = '2px 8px'; floatingBadge.style.borderRadius = '3px'; floatingBadge.style.cursor = 'pointer'; floatingBadge.style.fontWeight = 'bold'; floatingBadge.style.letterSpacing = '0.08em'; const panel = document.createElement('div'); panel.style.position = 'fixed'; panel.style.right = '10px'; panel.style.bottom = '22px'; panel.style.background = 'rgba(0,0,0,0.9)'; panel.style.color = '#fff'; panel.style.padding = '6px 10px'; panel.style.fontSize = '12px'; panel.style.borderRadius = '4px'; panel.style.maxWidth = '320px'; panel.style.minWidth = '240px'; panel.style.maxHeight = 'calc(100vh - 40px)'; // Prevent exceeding screen height panel.style.overflowY = 'auto'; // Make scrollable panel.style.boxSizing = 'border-box'; panel.style.boxShadow = '0 0 6px rgba(0,0,0,0.7)'; panel.style.display = 'none'; panel.innerHTML = `
Flash Yank
×
`; wrapper.appendChild(panel); wrapper.appendChild(floatingBadge); document.body.appendChild(wrapper); let panelVisible = false; const HOVER_MARGIN = 50; // px around panel before closing let activePlayer = null; function showPanel(trigger) { if (panelVisible) return; panelVisible = true; panel.style.display = 'block'; activePlayer = trigger.closest('.v0ck'); if (activePlayer) activePlayer.classList.add('v0ck_swf_active'); } function hidePanel() { if (!panelVisible) return; panelVisible = false; panel.style.display = 'none'; if (activePlayer) { activePlayer.classList.remove('v0ck_swf_active'); activePlayer = null; } } function handleBadgeHover(target) { if (target.id === 'toggleswf' || target === floatingBadge) { // Anchor to trigger element (shared logic for mobile/desktop) const rect = target.getBoundingClientRect(); panel.style.bottom = (window.innerHeight - rect.top + 10) + 'px'; panel.style.right = (window.innerWidth - rect.right) + 'px'; panel.style.transform = 'none'; panel.style.maxWidth = isMobile ? '90vw' : '320px'; showPanel(target); } } document.addEventListener('mouseover', (e) => { const target = e.target.closest('#toggleswf') || (e.target === floatingBadge ? floatingBadge : null); if (target) { handleBadgeHover(target); } }); document.addEventListener('mousemove', (e) => { if (!panelVisible || isMobile) return; // Don't hide by mousemove on mobile const rect = panel.getBoundingClientRect(); const left = rect.left - HOVER_MARGIN; const right = rect.right + HOVER_MARGIN; const top = rect.top - HOVER_MARGIN; const bottom = rect.bottom + HOVER_MARGIN; if (e.clientX < left || e.clientX > right || e.clientY < top || e.clientY > bottom) { hidePanel(); } }); const title = panel.querySelector('#w0bm-title'); const slider = panel.querySelector('#w0bm-yank-slider'); const yankValue = panel.querySelector('#w0bm-yank-value'); const info = panel.querySelector('#w0bm-yank-info'); ui = { wrapper, floatingBadge, panel, title, slider, yankValue, info }; slider.addEventListener('input', (e) => { const val = parseInt(e.target.value, 10); settings.yank = isNaN(val) ? 0 : Math.min(100, Math.max(0, val)); // Yank drives the underlying advanced parameters updateAdvancedFromYank(); saveSettings(); applySettingsToRuntime(); }); function toggleEnabledFromUI(targetController) { settings.enabled = !settings.enabled; saveSettings(); // If a specific controller was the target (e.g. clicked inside a player), use it. // Otherwise use the global currentController (main player). const controller = targetController || currentController; if (controller) { if (settings.enabled) controller.enable(); else controller.disable(); } // For global consistency, if settings.enabled changed, we might want to toggle ALL? // But per user request, we focus on the item player. // If there's another video that isn't the currentController, it won't toggle here, // but the hotkey and UI rely on currentController. updateUIFromSettings(); } // Click SWF badge or title to toggle filter enabled/disabled document.addEventListener('click', (e) => { const swfBtn = e.target.closest('#toggleswf'); const isBadge = swfBtn || e.target === floatingBadge; const isTitle = e.target === title; if (isBadge || isTitle) { // If clicked a button inside a player, try to get THAT player's controller let targetCtrl = null; if (swfBtn) { const player = swfBtn.closest('.v0ck'); const vid = player ? player.querySelector('video') : null; if (vid && vid.__flashFilterController) { targetCtrl = vid.__flashFilterController; } } toggleEnabledFromUI(targetCtrl); // On mobile, explicitly show panel on click/tap if (isMobile) { handleBadgeHover(swfBtn || floatingBadge); } } }); const closeBtn = panel.querySelector('#w0bm-close'); if (closeBtn) { closeBtn.addEventListener('click', () => { hidePanel(); }); } // Close Flash Yank panel when the main settings menu is closed document.addEventListener('v0ck_settings_closed', () => { hidePanel(); }); updateUIFromSettings(); } function updateUIFromSettings() { if (!ui) return; const isItem = isItemPage(); const hasVideos = document.querySelectorAll('video').length > 0; if (!isItem || !hasVideos) { ui.wrapper.style.display = 'none'; return; } ui.slider.value = settings.yank; ui.yankValue.textContent = settings.yank + '%'; const approxColors = Math.pow(currentConfig.paletteLevels, 3); ui.info.textContent = currentConfig.internalWidth + 'px width, ' + currentConfig.fps + 'fps, ~' + approxColors + ' colors'; // Enable/disable controls based on enabled state const isEnabled = !!settings.enabled; ui.slider.disabled = !isEnabled; // Visual state: strike-through when disabled const swfButtons = Array.from(document.querySelectorAll('.v0ck_menu_item')).filter(b => b.textContent.trim() === 'SWF'); // Handle floating badge visibility ui.floatingBadge.style.display = swfButtons.length > 0 ? 'none' : 'block'; // Style both (if they exist) [ui.floatingBadge, ...swfButtons].forEach(b => { if (!b) return; b.style.textDecoration = isEnabled ? 'none' : 'line-through'; b.style.opacity = isEnabled ? '1' : '0.6'; if (b.classList.contains('v0ck_menu_item')) { b.style.color = isEnabled ? 'var(--accent, #9f0)' : '#fff'; b.style.fontWeight = isEnabled ? 'bold' : 'normal'; } }); } // ---------- Hotkey ---------- function attachHotkey() { if (hotkeyAttached) return; hotkeyAttached = true; document.addEventListener('keydown', (e) => { if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || !isItemPage()) return; if (e.key.toLowerCase() !== 's') return; // Ignore when typing in an input, textarea, or contenteditable const tag = document.activeElement?.tagName?.toLowerCase(); if (tag === 'input' || tag === 'textarea' || document.activeElement?.isContentEditable) return; if (!currentController) return; settings.enabled = !settings.enabled; if (settings.enabled) currentController.enable(); else currentController.disable(); saveSettings(); updateUIFromSettings(); if (typeof window.flashMessage === 'function') { window.flashMessage(`Flash Yank ${settings.enabled ? 'enabled' : 'disabled'}`, 2000, settings.enabled ? 'success' : 'error'); } }); } // ---------- Bootstrapping ---------- function onReady(fn) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', fn, { once: true }); } else { fn(); } } onReady(() => { // firefox mobile check const isFirefoxMobile = /android|iphone|ipad|ipod/i.test(navigator.userAgent) && /firefox/i.test(navigator.userAgent); if (isFirefoxMobile) { console.log("Firefox Mobile detected, disabling Flash Yank script."); return; } applySettingsToRuntime(); createOptionsUI(); scanExistingVideos(); startObserver(); attachHotkey(); // Path change handling for AJAX transitions const handlePathChange = () => { // Reset currentController on path change to force re-discovery currentController = null; if (!isItemPage()) { if (ui) ui.wrapper.style.display = 'none'; } else { // Scan after a short delay to allow DOM/v0ck to settle setTimeout(() => { scanExistingVideos(); updateUIFromSettings(); }, 100); } }; window.addEventListener('popstate', handlePathChange); document.addEventListener('f0ck:contentLoaded', handlePathChange); }); })();