// ==UserScript==
// @name /vt/ Soundpost Alternatives
// @namespace https://tampermonkey.net/
// @version 2.7
// @description Fixed playback bug, removed forced muting, added draggable converter, added Catbox auto-embed, and fixed leftover (embed) text.
// @match https://boards.4chan.org/vt/*
// @match https://boards.4chan.org/wsg/*
// @grant GM_setClipboard
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const BOARD = location.pathname.split('/')[1];
const IS_VT = BOARD === 'vt';
const IS_WSG = BOARD === 'wsg';
let settings = {
autoEmbed: GM_getValue('autoEmbed', true),
hideCrosslink: GM_getValue('hideCrosslink', true),
autoEmbedCatbox: GM_getValue('autoEmbedCatbox', false),
hideCatboxLink: GM_getValue('hideCatboxLink', false)
};
function saveSettings() {
GM_setValue('autoEmbed', settings.autoEmbed);
GM_setValue('hideCrosslink', settings.hideCrosslink);
GM_setValue('autoEmbedCatbox', settings.autoEmbedCatbox);
GM_setValue('hideCatboxLink', settings.hideCatboxLink);
}
// ==================== /vt/ DRAGGABLE ARROW + PANEL ====================
if (IS_VT) {
const arrow = document.createElement('div');
Object.assign(arrow.style, {
position: 'fixed',
right: '8px',
top: '45%',
background: 'rgba(0,0,0,0.65)',
color: '#fff',
padding: '14px 7px',
fontSize: '20px',
fontFamily: 'monospace',
cursor: 'grab',
zIndex: '99999',
borderRadius: '8px 0 0 8px',
opacity: '0.4',
transition: 'opacity 0.2s',
userSelect: 'none'
});
arrow.innerHTML = '<';
arrow.title = 'VT-WSG Tools (drag or click)';
document.body.appendChild(arrow);
let isDragging = false;
let hasMoved = false;
let startY = 0;
arrow.addEventListener('mousedown', (e) => {
isDragging = true;
hasMoved = false;
startY = e.clientY;
arrow.style.transition = 'none';
arrow.style.cursor = 'grabbing';
const rect = arrow.getBoundingClientRect();
arrow.style.right = 'auto';
arrow.style.left = rect.left + 'px';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
if (Math.abs(e.clientY - startY) > 8) hasMoved = true;
let newTop = e.clientY - (arrow.offsetHeight / 2);
newTop = Math.max(30, Math.min(window.innerHeight - 100, newTop));
arrow.style.top = newTop + 'px';
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
arrow.style.transition = 'opacity 0.2s';
arrow.style.cursor = 'grab';
if (!hasMoved) togglePanel();
}
});
function togglePanel() {
panel.style.display = panel.style.display === 'block' ? 'none' : 'block';
arrow.style.opacity = panel.style.display === 'block' ? '1' : '0.4';
}
const panel = document.createElement('div');
Object.assign(panel.style, {
position: 'fixed',
right: '70px',
top: '35%',
background: 'rgba(25,25,25,0.96)',
color: '#eee',
padding: '16px',
borderRadius: '8px',
zIndex: '100000',
width: '275px',
display: 'none',
boxShadow: '0 5px 15px rgba(0,0,0,0.7)',
fontFamily: 'sans-serif'
});
panel.innerHTML = `
/vt/ Soundpost Alternatives
Drag arrow anywhere.
Videos start paused.
`;
document.body.appendChild(panel);
setTimeout(() => {
const cbEmbed = panel.querySelector('#cb-embed');
const cbHide = panel.querySelector('#cb-hide');
const cbCatboxEmbed = panel.querySelector('#cb-catbox-embed');
const cbCatboxHide = panel.querySelector('#cb-catbox-hide');
const postBtn = panel.querySelector('#btn-post');
cbEmbed.checked = settings.autoEmbed;
cbHide.checked = settings.hideCrosslink;
cbCatboxEmbed.checked = settings.autoEmbedCatbox;
cbCatboxHide.checked = settings.hideCatboxLink;
cbEmbed.addEventListener('change', () => { settings.autoEmbed = cbEmbed.checked; saveSettings(); });
cbHide.addEventListener('change', () => {
settings.hideCrosslink = cbHide.checked;
saveSettings();
document.querySelectorAll('.quotelink[data-embedded]').forEach(link => {
link.style.display = settings.hideCrosslink ? 'none' : '';
});
});
cbCatboxEmbed.addEventListener('change', () => { settings.autoEmbedCatbox = cbCatboxEmbed.checked; saveSettings(); });
// Updated toggle logic to also hide/show the adjacent (embed) text if it exists
cbCatboxHide.addEventListener('change', () => {
settings.hideCatboxLink = cbCatboxHide.checked;
saveSettings();
document.querySelectorAll('a[href*="catbox.moe"][data-embedded]').forEach(link => {
link.style.display = settings.hideCatboxLink ? 'none' : '';
let embedSibling = link.nextElementSibling;
if (embedSibling && embedSibling.classList.contains('catbox-native-embed')) {
embedSibling.style.display = settings.hideCatboxLink ? 'none' : '';
}
});
});
postBtn.addEventListener('click', () => window.location.href = "https://boards.4chan.org/wsg/catalog#s=gvy");
}, 200);
}
// ==================== /wsg/ DRAGGABLE & COLLAPSIBLE BOX ====================
if (IS_WSG) {
function addCrossButtons() {
document.querySelectorAll('.postContainer:not(.cross-processed)').forEach(post => {
const postId = post.id.replace(/\D/g, '');
if (!postId) return;
const postno = post.querySelector('.postno, .postNum');
if (!postno) return;
const btn = document.createElement('span');
btn.textContent = ' [Cross]';
btn.style.cssText = 'color:#0a0;cursor:pointer;font-size:0.85em;';
btn.onclick = () => {
GM_setClipboard(`>>>/wsg/${postId}`);
btn.textContent = ' [Copied!]';
setTimeout(() => btn.textContent = ' [Cross]', 1500);
};
postno.appendChild(btn);
post.classList.add('cross-processed');
});
}
new MutationObserver(addCrossButtons).observe(document.body, {childList:true, subtree:true});
window.addEventListener('load', addCrossButtons);
const box = document.createElement('div');
Object.assign(box.style, {
position: 'fixed',
bottom: '18px',
right: '18px',
background: 'rgba(0,0,0,0.85)',
border: '1px solid #0a0',
borderRadius: '8px',
width: '230px',
zIndex: '99999',
color: '#ddd',
fontSize: '13px',
fontFamily: 'sans-serif',
overflow: 'hidden',
boxShadow: '0 4px 10px rgba(0,0,0,0.5)'
});
box.innerHTML = `
`;
document.body.appendChild(box);
const header = box.querySelector('#converter-header');
const body = box.querySelector('#converter-body');
const toggleBtn = box.querySelector('#converter-toggle');
const input = box.querySelector('#input');
const status = box.querySelector('#status');
// Collapsing Logic
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (body.style.display === 'none') {
body.style.display = 'block';
toggleBtn.textContent = '[-]';
header.style.borderBottom = '1px solid #0a0';
} else {
body.style.display = 'none';
toggleBtn.textContent = '[+]';
header.style.borderBottom = 'none';
}
});
// Dragging Logic
let isBoxDragging = false;
let boxStartX, boxStartY, initialMouseX, initialMouseY;
header.addEventListener('mousedown', (e) => {
if (e.target === toggleBtn) return;
isBoxDragging = true;
header.style.cursor = 'grabbing';
const rect = box.getBoundingClientRect();
boxStartX = rect.left;
boxStartY = rect.top;
initialMouseX = e.clientX;
initialMouseY = e.clientY;
box.style.bottom = 'auto';
box.style.right = 'auto';
box.style.left = boxStartX + 'px';
box.style.top = boxStartY + 'px';
});
document.addEventListener('mousemove', (e) => {
if (!isBoxDragging) return;
e.preventDefault();
const dx = e.clientX - initialMouseX;
const dy = e.clientY - initialMouseY;
box.style.left = (boxStartX + dx) + 'px';
box.style.top = (boxStartY + dy) + 'px';
});
document.addEventListener('mouseup', () => {
if (isBoxDragging) {
isBoxDragging = false;
header.style.cursor = 'grab';
}
});
// Conversion Logic
function convert() {
const val = input.value.trim();
const match = val.match(/>>?(\d+)/);
if (!match) {
status.innerHTML = 'Enter something like >>12345678';
return;
}
const cross = `>>>/wsg/${match[1]}`;
GM_setClipboard(cross);
status.innerHTML = `[OK] Copied: ${cross}`;
setTimeout(() => status.textContent = '', 2800);
}
input.addEventListener('keypress', e => { if (e.key === 'Enter') convert(); });
input.addEventListener('input', () => { if (input.value.trim().length > 6) convert(); });
}
// ==================== AUTO-EMBED VIDEOS ON /vt/ ====================
if (IS_VT) {
// Helper function for the clickable expanding video behavior
function bindVideoClickBehavior(video) {
let expanded = false;
video.addEventListener('click', (e) => {
const rect = video.getBoundingClientRect();
const clickY = e.clientY - rect.top;
if (expanded && clickY > rect.height - 40) return;
e.preventDefault();
e.stopImmediatePropagation();
expanded = !expanded;
if (expanded) {
video.style.maxWidth = '88vw';
video.style.maxHeight = '75vh';
let playPromise = video.play();
if (playPromise !== undefined) {
playPromise.catch(err => console.debug('Playback interrupted:', err));
}
} else {
video.style.maxWidth = '340px';
video.style.maxHeight = '';
video.pause();
}
});
}
async function embedVideo(link) {
if (link.dataset.embedded === 'true') return;
link.dataset.embedded = 'true';
if (link.nextElementSibling && link.nextElementSibling.tagName === 'VIDEO') return;
try {
let href = link.href.startsWith('//') ? 'https:' + link.href : link.href;
const match = href.match(/\/wsg\/(?:thread|res)\/(\d+)/);
if (!match) return;
const threadId = match[1];
const postMatch = href.match(/#p?(\d+)/) || link.textContent.match(/(\d+)/);
const postId = postMatch ? postMatch[1] : threadId;
const json = await fetch(`https://a.4cdn.org/wsg/thread/${threadId}.json`).then(r => r.json());
const post = json.posts.find(p => String(p.no) === postId);
if (!post || !['.webm','.mp4','.gif'].includes(post.ext || '')) return;
const videoUrl = `https://i.4cdn.org/wsg/${post.tim}${post.ext}`;
const video = document.createElement('video');
Object.assign(video, {
src: videoUrl,
controls: true,
loop: true,
style: 'max-width: 340px; margin: 8px 0; display:block; border:1px solid #444; border-radius:4px; cursor:pointer;'
});
video.pause();
bindVideoClickBehavior(video);
link.after(video);
if (settings.hideCrosslink) {
link.style.display = 'none';
}
} catch (e) {
console.debug('Embed skipped', e);
}
}
async function embedCatbox(link) {
if (link.dataset.embedded === 'true') return;
if (!link.href.match(/\.(webm|mp4|gif)$/i)) return;
link.dataset.embedded = 'true';
if (link.nextElementSibling && link.nextElementSibling.tagName === 'VIDEO') return;
try {
const video = document.createElement('video');
Object.assign(video, {
src: link.href,
controls: true,
loop: true,
style: 'max-width: 340px; margin: 8px 0; display:block; border:1px solid #444; border-radius:4px; cursor:pointer;'
});
video.pause();
bindVideoClickBehavior(video);
// Check for a native or 4chanX (embed) button directly following the link
let embedSibling = link.nextElementSibling;
if (embedSibling && embedSibling.tagName === 'A' && embedSibling.textContent.toLowerCase().includes('embed')) {
embedSibling.classList.add('catbox-native-embed');
embedSibling.after(video); // Place the video after the embed text
} else {
link.after(video);
}
if (settings.hideCatboxLink) {
link.style.display = 'none';
if (embedSibling && embedSibling.classList.contains('catbox-native-embed')) {
embedSibling.style.display = 'none';
}
}
} catch (e) {
console.debug('Catbox embed skipped', e);
}
}
function scan() {
if (settings.autoEmbed) {
document.querySelectorAll('.quotelink[href*="wsg"]:not([data-embedded])').forEach(link => {
embedVideo(link);
});
}
if (settings.autoEmbedCatbox) {
document.querySelectorAll('a[href*="catbox.moe"]:not([data-embedded])').forEach(link => {
embedCatbox(link);
});
}
}
new MutationObserver(scan).observe(document.body, {childList:true, subtree:true});
window.addEventListener('load', scan);
}
console.log('%cVT-WSG Crosslinker v2.7 loaded on /' + BOARD, 'color:#0a0;font-weight:bold');
})();