init f0ckm
This commit is contained in:
723
public/s/js/flash_yank.js
Normal file
723
public/s/js/flash_yank.js
Normal file
@@ -0,0 +1,723 @@
|
||||
|
||||
(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 = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<div id="w0bm-title" style="font-weight:bold; cursor:pointer;">Flash Yank</div>
|
||||
<div id="w0bm-close" style="cursor:pointer; font-size:16px; padding: 0 4px;">×</div>
|
||||
</div>
|
||||
|
||||
<label style="font-size:11px; display:block; margin-bottom:2px;">
|
||||
Yank: <span id="w0bm-yank-value"></span>
|
||||
</label>
|
||||
<input id="w0bm-yank-slider" type="range" min="0" max="100" step="1" style="width:100%;">
|
||||
|
||||
<div id="w0bm-yank-info" style="font-size:10px; margin-top:8px; opacity: 0.8;"></div>
|
||||
`;
|
||||
|
||||
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);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user