您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Play sound for soundposts (video and image) on Holotower.
// ==UserScript== // @name Holotower Soundposts // @namespace http://tampermonkey.net/ // @version 1.2 // @author grem // @license MIT // @description Play sound for soundposts (video and image) on Holotower. // @match https://boards.holotower.org/* // @match https://holotower.org/* // @grant none // @icon https://boards.holotower.org/favicon.gif // @run-at document-end // ==/UserScript== (() => { 'use strict'; const SITE_VOLUME_KEY = 'videovolume'; const VOLUME_FALLBACK = 1.0; const soundMap = new Map(); // filePath → sound URL const active = new Map(); // media element → { audio, visibilityObserver, listeners } const NATIVE_EXPANDED_IMAGE_SELECTOR = 'img.full-image'; const NATIVE_HOVER_IMAGE_SELECTOR = '#chx_hoverImage'; const IQ_HOVER_IMAGE_SELECTOR = 'img[style*="position: fixed"][style*="pointer-events: none"]'; const ALL_IMAGE_SELECTORS = `${NATIVE_EXPANDED_IMAGE_SELECTOR}, ${NATIVE_HOVER_IMAGE_SELECTOR}, ${IQ_HOVER_IMAGE_SELECTOR}`; const siteVolume = () => { let v = localStorage.getItem(SITE_VOLUME_KEY); if (typeof v === "string" && v.startsWith('"') && v.endsWith('"')) { v = v.slice(1, -1); } v = parseFloat(v); if (!isFinite(v) || v < 0 || v > 1) v = VOLUME_FALLBACK; return v; }; const tagIcon = span => { if (span.dataset.soundControlsAdded) return; span.dataset.soundControlsAdded = 'true'; const mark = document.createElement('span'); mark.textContent = ' 🔊'; mark.title = 'This file has sound.'; span.after(mark); }; function scan(root = document) { root.querySelectorAll('p.fileinfo a[download]').forEach(a => { const span = a.closest('span.unimportant'); if (!span) return; const m = a.download.match(/\[sound=([^\]]+)]/i); if (!m) return; try { const url = decodeURIComponent(decodeURIComponent(m[1])); const fullURL = url.startsWith('http') ? url : `https://${url}`; const filePath = new URL(a.getAttribute('href'), location.href).pathname; soundMap.set(filePath, fullURL); tagIcon(span); } catch {} }); } function isDisplayVisible(el) { if (!el) return false; const style = el.style.display || window.getComputedStyle(el).display; return style === "block" || style === "inline" || style === ""; } // SOUNDPOST VIDEO HANDLER function bind(video) { const path = new URL(video.src, location.href).pathname; const soundURL = soundMap.get(path); if (!soundURL || active.has(video)) return; let container = video.parentElement; for (let i = 0; i < 2; ++i) { if (!container) break; if (container.style && (container.style.display !== undefined)) break; container = container.parentElement; } if (!container || container === document.body) container = video; const audio = new Audio(soundURL); audio.preload = 'auto'; audio.loop = true; audio.volume = video.volume ?? siteVolume(); audio.muted = video.muted; video.loop = true; const listeners = {}; listeners.tighten = () => { const d = video.duration || 1; const target = audio.currentTime % d; if (Math.abs(video.currentTime - target) > 0.25) { video.currentTime = target; } if (video.paused) video.play().catch(()=>{}); }; listeners.tryPlayAudio = () => { if (!audio.paused && !video.paused) return; if (isDisplayVisible(container) && !video.paused) { listeners.tighten(); audio.play().catch(()=>{}); } }; listeners.tryPauseAudio = () => { if (!isDisplayVisible(container) && !audio.paused) { audio.pause(); } }; listeners.onVideoPause = () => { if (!audio.paused) audio.pause(); }; listeners.onVolumeChange = () => { audio.volume = video.volume; audio.muted = video.muted; }; audio.addEventListener('timeupdate', listeners.tighten); video.addEventListener('play', listeners.tryPlayAudio); video.addEventListener('pause', listeners.onVideoPause); video.addEventListener('volumechange', listeners.onVolumeChange); const visibilityObserver = new MutationObserver(() => { const visible = isDisplayVisible(container); if (visible) { listeners.tryPlayAudio(); } else { listeners.tryPauseAudio(); } }); visibilityObserver.observe(container, { attributes: true, attributeFilter: ["style"] }); if (isDisplayVisible(container) && !video.paused) { listeners.tryPlayAudio(); } active.set(video, { audio, visibilityObserver, listeners }); } // SOUNDPOST IMAGE HANDLER function bindImage(img) { const path = new URL(img.src, location.href).pathname; const soundURL = soundMap.get(path); if (!soundURL || active.has(img)) return; let container = img.parentElement; if (img.matches(ALL_IMAGE_SELECTORS)) { container = img; } else { for (let i = 0; i < 2; ++i) { if (!container) break; if (container.style && (container.style.display !== undefined)) break; container = container.parentElement; } if (!container || container === document.body) container = img; } const audio = new Audio(soundURL); audio.preload = 'auto'; audio.loop = true; const listeners = {}; listeners.tryPlayAudio = () => { if (!audio.paused && isDisplayVisible(container)) return; if (isDisplayVisible(container)) { audio.volume = siteVolume(); audio.currentTime = 0; audio.play().catch(()=>{}); } }; listeners.tryPauseAudio = () => { if (!isDisplayVisible(container) && !audio.paused) { audio.pause(); audio.currentTime = 0; } }; const visibilityObserver = new MutationObserver(() => { const visible = isDisplayVisible(container); if (visible) { listeners.tryPlayAudio(); } else { listeners.tryPauseAudio(); } }); visibilityObserver.observe(container, { attributes: true, attributeFilter: ["style"] }); if (isDisplayVisible(container)) { listeners.tryPlayAudio(); } active.set(img, { audio, visibilityObserver, listeners: {} }); } function scanImages(root=document) { root.querySelectorAll(ALL_IMAGE_SELECTORS).forEach(bindImage); } // CENTRALIZED OBSERVER FOR ADDING AND REMOVING NODES const mainObserver = new MutationObserver(mutations => { for (const mutation of mutations) { // Handle newly added nodes for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; if (node.matches('.post, .reply')) scan(node); node.querySelectorAll?.('.post, .reply').forEach(scan); if (node.matches('video')) bind(node); node.querySelectorAll?.('video').forEach(bind); if (node.matches(ALL_IMAGE_SELECTORS)) bindImage(node); node.querySelectorAll?.(ALL_IMAGE_SELECTORS).forEach(bindImage); } // Handle removed nodes (GARBAGE COLLECTION) for (const removedNode of mutation.removedNodes) { if (removedNode.nodeType !== 1) continue; active.forEach((value, key) => { if (removedNode === key || removedNode.contains(key)) { // Full teardown to prevent zombie listeners value.audio.pause(); value.visibilityObserver.disconnect(); if (value.listeners.tighten) { value.audio.removeEventListener('timeupdate', value.listeners.tighten); key.removeEventListener('play', value.listeners.tryPlayAudio); key.removeEventListener('pause', value.listeners.onVideoPause); key.removeEventListener('volumechange', value.listeners.onVolumeChange); } active.delete(key); } }); } } }); // BOOTSTRAP scan(); document.querySelectorAll('video').forEach(bind); scanImages(); mainObserver.observe(document.body, { childList: true, subtree: true }); function patchPostCloning() { const postProto = window.g?.Post?.prototype; if (typeof postProto?.addClone !== 'function') { if ((patchPostCloning.attempts || 0) < 5) { setTimeout(patchPostCloning, 500); patchPostCloning.attempts = (patchPostCloning.attempts || 0) + 1; } return; } const originalAddClone = postProto.addClone; if (originalAddClone.isPatchedBySoundposts) return; postProto.addClone = function(...args) { const cloneObj = originalAddClone.apply(this, args); if (cloneObj?.nodes?.root) { const clonedPost = cloneObj.nodes.root; scan(clonedPost); clonedPost.querySelectorAll('video').forEach(bind); scanImages(clonedPost); } return cloneObj; }; postProto.addClone.isPatchedBySoundposts = true; } patchPostCloning(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址