// ==UserScript==
// @name 4chan sounds
// @version 0.1.0
// @namespace rccom
// @description Play that faggy music weeb boi
// @author RCC
// @match *://boards.4chan.org/*
// @match *://boards.4channel.org/*
// @grant GM.getValue
// @grant GM.setValue
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
let isChanX;
const allow = [
"4cdn.org",
"catbox.moe",
"dmca.gripe",
"lewd.se",
"pomf.cat",
"zz.ht"
]
const Player = {
ns: 'fc-sounds',
sounds: [],
container: null,
ui: {},
settings: {
shuffle: false,
repeat: 'all'
},
colors: {
background: '#d6daf0',
border: '#b7c5d9',
expander: '#808bbf',
expander_hover: '#9aa6e1',
playing: '#98bff7'
},
_templates: {
css: ({ ns }) =>
`#${ns}-container {
position: fixed;
background: ${Player.colors.background};
border: 1px solid ${Player.colors.border};
display: relative;
min-height: 200px;
min-width: 100px;
}
.${ns}-show-settings .${ns}-player {
display: none;
}
.${ns}-setting {
display: none;
}
.${ns}-show-settings .${ns}-settings {
display: block;
}
.${ns}-title {
cursor: grab;
text-align: center;
border-bottom: solid 1px ${Player.colors.border};
padding: .25rem 0;
}
html.fourchan-x .${ns}-title a {
font-size: 0;
visibility: hidden;
margin: 0 0.25rem;
}
.${ns}-image-link {
height: 128px;
text-align: center;
display: flex;
justify-items: center;
justify-content: center;
border-bottom: solid 1px ${Player.colors.border};
}
.${ns}-image-link .${ns}-video {
display: none;
}
.${ns}-image-link.${ns}-show-video .${ns}-video {
display: block;
}
.${ns}-image-link.${ns}-show-video .${ns}-image {
display: none;
}
.${ns}-image, .${ns}-video {
max-height: 125px;
}
.${ns}-audio {
width: 100%;
}
.${ns}-list-container {
overflow: scroll;
}
.${ns}-list {
display: grid;
list-style-type: none;
padding: 0;
margin: 0;
}
.${ns}-list-item {
list-style-type: none;
padding: 0.15rem 0.25rem;
white-space: nowrap;
cursor: pointer;
}
.${ns}-list-item.playing {
background: ${Player.colors.playing}
}
.${ns}-list-item:nth-child(2n) {
background: ${Player.colors.border};
}
.${ns}-expander {
position: absolute;
bottom: 0px;
right: 0px;
height: 12px;
width: 12px;
cursor: se-resize;
background: linear-gradient(to bottom right, rgba(0,0,0,0), rgba(0,0,0,0) 50%, ${Player.colors.expander} 55%, ${Player.colors.expander} 100%)
}
.${ns}-expander:hover {
background: linear-gradient(to bottom right, rgba(0,0,0,0), rgba(0,0,0,0) 50%, ${Player.colors.expander_hover} 55%, ${Player.colors.expander_hover} 100%)
}`,
body: ({ ns }) =>
`<div id="${ns}-container" style="top: 100px; left: 100px; width: 230px;">
<div class="${ns}-title">
<span style="float: left">
<a class="${ns}-shuffle-button fa fa-random ${Player.settings.shuffle ? '' : 'disabled'}" title="Shuffle" href="javascript;">Shuffle</a>
</span>
4chan Sounds
<span style="float: right">
<a class="${ns}-config-button ${ns}-title-right fa fa-wrench" title="Settings" href="javascript;">Settings</a>
</span>
</div>
<div class="${ns}-player">
<a class="${ns}-image-link" target="_blank">
<img class="${ns}-image"></img>
<video class="${ns}-video"></video>
</a>
<audio class="${ns}-audio" controls="true"></audio>
<div class="${ns}-list-container"><ul class="${ns}-list"></ul></div>
</div>
<div class="${ns}-settings">
</div>
<div class="${ns}-expander"></div>
</div>`,
list: ({ ns }) =>
Player.sounds.map(sound => `<li class="${ns}-list-item" data-id="${sound.id}">${sound.title}</li>`).join('')
},
initialize: function () {
Player.sounds = [ ];
Player.playOrder = [ ];
Player._tplOptions = { ns: Player.ns };
},
render: async function () {
if (Player.container) {
document.body.removeChild(Player.container);
}
// Insert the stylesheet
const stylesheet = document.createElement('style');
stylesheet.innerHTML = Player._templates.css(Player._tplOptions);
document.head.appendChild(stylesheet);
// Create the main player
const el = document.createElement('div');
el.innerHTML = Player._templates.body(Player._tplOptions);
Player.container = el.querySelector(`#${Player.ns}-container`);
document.body.appendChild(Player.container);
Player.ui.title = Player.container.querySelector(`.${Player.ns}-title`);
Player.ui.shuffleButton = Player.container.querySelector(`.${Player.ns}-shuffle-button`);
Player.ui.configButton = Player.container.querySelector(`.${Player.ns}-config-button`)
Player.ui.imageLink = Player.container.querySelector(`.${Player.ns}-image-link`);
Player.ui.image = Player.container.querySelector(`.${Player.ns}-image`);
Player.ui.video = Player.container.querySelector(`.${Player.ns}-video`);
Player.ui.listContainer = Player.container.querySelector(`.${Player.ns}-list-container`);
Player.ui.list = Player.container.querySelector(`.${Player.ns}-list`);
Player.ui.expander = Player.container.querySelector(`.${Player.ns}-expander`);
Player.audio = Player.container.querySelector(`.${Player.ns}-audio`);
// Add the event listeners for selecting a song
Player.ui.list.addEventListener('click', function (e) {
const id = e.target.getAttribute('data-id');
const sound = id && Player.sounds.find(function (sound) {
return sound.id === '' + id;
});
sound && Player.play(sound);
});
// Add event listeners for the title buttons
Player.ui.configButton.addEventListener('click', Player.toggleSettings);
Player.ui.shuffleButton.addEventListener('click', Player.toggleShuffle);
// Add event listeners for moving/resizing
Player.ui.expander.addEventListener('mousedown', Player.initResize, false);
Player.ui.title.addEventListener('mousedown', Player.initMove, false);
// Add event listener for music ending
Player.audio.addEventListener('ended', Player.next);
Player.audio.addEventListener('pause', () => Player.ui.video.pause());
Player.audio.addEventListener('play', () => Player.ui.video.play());
Player.audio.addEventListener('seeked', () => Player.ui.video.currentTime = Player.audio.currentTime);
// Render the list
Player.renderList();
// Apply the last position/size
const [ top, left ] = (await GM.getValue(Player.ns + '.position') || '').split(':');
const [ width, height ] = (await GM.getValue(Player.ns + '.size') || '').split(':');
+width && +height && Player.resizeTo(width, height);
+top && +left && Player.moveTo(top, left);
},
renderList: function () {
const list = Player.container.querySelector(`.${Player.ns}-list`);
list.innerHTML = Player._templates.list(Player._tplOptions);
},
toggleSettings: function (e) {
e.preventDefault();
if (Player.container.classList.contains(Player.ns + '-show-settings')) {
Player.container.classList.remove(Player.ns + '-show-settings');
} else {
Player.container.classList.add(Player.ns + '-show-settings');
}
},
toggleShuffle: function (e) {
e.preventDefault();
Player.settings.shuffle = !Player.settings.shuffle;
const action = Player.settings.shuffle ? 'remove' : 'add';
Player.ui.shuffleButton.classList[action]('disabled');
if (!Player.settings.shuffle) {
Player.playOrder = [ ...Player.sounds ];
} else {
const playOrder = Player.playOrder;
for (let i = playOrder.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[playOrder[i], playOrder[j]] = [playOrder[j], playOrder[i]];
}
}
},
initResize: function initDrag(e) {
disableUserSelect();
Player._startX = e.clientX;
Player._startY = e.clientY;
Player._startWidth = parseInt(document.defaultView.getComputedStyle(Player.container).width, 10);
Player._startHeight = parseInt(document.defaultView.getComputedStyle(Player.container).height, 10);
document.documentElement.addEventListener('mousemove', Player.doResize, false);
document.documentElement.addEventListener('mouseup', Player.stopResize, false);
},
doResize: function(e) {
Player.resizeTo(Player._startWidth + e.clientX - Player._startX, Player._startHeight + e.clientY - Player._startY);
},
resizeTo: function (width, height) {
Player.container.style.width = width + 'px';
Player.ui.listContainer.style.height = Math.max(10, height - 194) + 'px';
},
stopResize: function(e) {
const style = document.defaultView.getComputedStyle(Player.container);
document.documentElement.removeEventListener('mousemove', Player.doResize, false);
document.documentElement.removeEventListener('mouseup', Player.stopResize, false);
enableUserSelect();
GM.setValue(Player.ns + '.size', parseInt(style.width, 10) + ':' + parseInt(style.height, 10));
},
initMove: function (e) {
disableUserSelect();
Player.ui.title.style.cursor = 'grabbing';
Player._offsetX = e.clientX - Player.container.offsetLeft;
Player._offsetY = e.clientY - Player.container.offsetTop;
document.documentElement.addEventListener('mousemove', Player.doMove, false);
document.documentElement.addEventListener('mouseup', Player.stopMove, false);
},
doMove: function (e) {
Player.moveTo(e.clientX - Player._offsetX, e.clientY - Player._offsetY);
},
moveTo: function (x, y) {
const style = document.defaultView.getComputedStyle(Player.container);
const maxX = document.documentElement.clientWidth - parseInt(style.width, 10);
const maxY = document.documentElement.clientHeight - parseInt(style.height, 10);
Player.container.style.left = Math.max(0, Math.min(x, maxX)) + 'px';
Player.container.style.top = Math.max(0, Math.min(y, maxY)) + 'px';
},
stopMove: function (e) {
document.documentElement.removeEventListener('mousemove', Player.doMove, false);
document.documentElement.removeEventListener('mouseup', Player.stopMove, false);
Player.ui.title.style.cursor = null;
enableUserSelect();
GM.setValue(Player.ns + '.position', parseInt(Player.container.style.left, 10) + ':' + parseInt(Player.container.style.top, 10));
},
add: function (title, id, src, thumb, image) {
const sound = { title, src, id, thumb, image };
Player.sounds.push(sound);
// Add the sound to the play order at the end, or someone random for shuffled.
const index = Player.settings.shuffle
? Math.floor(Math.random() * Player.sounds.length - 1)
: Player.sounds.length;
Player.playOrder.splice(index, 0, sound);
// Render the player or re-render the list
if (!Player.container) {
Player.render();
} else {
Player.renderList();
}
// If nothing else has been added show the image for this sound.
if (Player.playOrder.length === 1) {
Player.showThumb(sound);
}
},
showThumb: function (sound) {
Player.ui.imageLink.classList.remove(Player.ns + '-show-video');
Player.ui.image.src = sound.thumb;
Player.ui.imageLink.href = sound.image;
},
playVideo: function (sound) {
Player.ui.imageLink.classList.add(Player.ns + '-show-video');
Player.ui.video.src = sound.image;
Player.ui.video.play();
},
play: function (sound) {
if (sound) {
if (Player.playing) {
const currentItem = Player.ui.list.querySelector('.playing');
currentItem && currentItem.classList.remove('playing');
}
const item = Player.ui.list.querySelector(`li[data-id="${sound.id}"]`);
item && item.classList.add('playing');
Player.playing = sound;
Player.audio.src = sound.src;
if (sound.image.endsWith('.webm')) {
Player.playVideo(sound);
} else {
Player.showThumb(sound);
}
}
Player.audio.play();
},
pause: function () {
Player.audio.pause();
},
next: function () {
Player._movePlaying(1);
},
previous: function () {
Player._movePlaying(-1);
},
_movePlaying: function (direction) {
// If there's no sound fall out.
if (!Player.playOrder.length) {
return;
}
// If there's no sound currently playing or it's not in the list then just play the first sound.
const currentIndex = Player.playOrder.indexOf(Player.playing);
if (currentIndex === -1) {
return Player.playSound(Player.playOrder[0]);
}
// Get the next index, either repeating the same, wrapping round to repeat all or just moving the index.
const nextIndex = Player.settings.repeat === 'one'
? currentIndex
: Player.settings.repeat === 'all'
? ((currentIndex + direction) + Player.playOrder.length) % Player.playOrder.length
: currentIndex + direction;
const nextSound = Player.playOrder[nextIndex];
nextSound && Player.play(nextSound);
}
};
let doInit = function () {
doInit = () => null;
parseFiles(document.body);
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type === "childList") {
mutation.addedNodes.forEach(function (node) {
if (node.nodeType === Node.ELEMENT_NODE) {
parseFiles(node);
}
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
};
document.addEventListener("DOMContentLoaded", function (event) {
setTimeout(function () {
if (document.body.classList.contains("ws") || document.body.classList.contains("nws")) {
isChanX = false;
doInit();
}
}, 1);
});
document.addEventListener( "4chanXInitFinished", function (event) {
if (document.documentElement.classList.contains("fourchan-x") && document.documentElement.classList.contains("sw-yotsuba")) {
isChanX = true;
doInit();
}
});
function parseFiles (target) {
target.querySelectorAll(".post").forEach(function (post) {
if (post.parentElement.parentElement.id === "qp" || post.parentElement.classList.contains("noFile")) {
return;
}
post.querySelectorAll(".file").forEach(function (file) {
parseFile(file, post);
});
});
};
function parseFile(file, post) {
if (!file.classList.contains("file")) {
return;
}
const fileLink = isChanX
? file.querySelector(".fileText .file-info > a")
: file.querySelector(".fileText > a");
if (!fileLink) {
return;
}
if (!fileLink.href) {
return;
}
let fileName = null;
if (isChanX) {
[
file.querySelector(".fileText .file-info .fnfull"),
file.querySelector(".fileText .file-info > a")
].some(function (node) {
return node && (fileName = node.textContent);
});
} else {
[
file.querySelector(".fileText"),
file.querySelector(".fileText > a")
].some(function (node) {
return node && (fileName = node.title || node.tagName === "A" && node.textContent);
});
}
if (!fileName) {
return;
}
fileName = fileName.replace(/\-/, "/");
const match = fileName.match(/^(.*)[\[\(\{](?:audio|sound)[ \=\:\|\$](.*?)[\]\)\}]/i);
if (!match) {
return;
}
const id = post.id.slice(1);
const name = match[1] || id;
const fileThumb = post.querySelector('.fileThumb');
const fullSrc = fileThumb && fileThumb.href;
const thumbSrc = fileThumb && fileThumb.querySelector('img').src;
let link = match[2];
if (link.includes("%")) {
try {
link = decodeURIComponent(link);
} catch (error) {
return;
}
}
if (link.match(/^(https?\:)?\/\//) === null) {
link = (location.protocol + "//" + link);
}
try {
link = new URL(link);
} catch (error) {
return;
}
for (let item of allow) {
if (link.hostname.toLowerCase() === item || link.hostname.toLowerCase().endsWith("." + item)) {
return Player.add(name, id, link.href, thumbSrc, fullSrc);
}
}
};
function disableUserSelect () {
document.body.style.userSelect = 'none';
document.body.style.MozUserSelect = 'none';
}
function enableUserSelect () {
document.body.style.userSelect = null;
document.body.style.MozUserSelect = null;
}
Player.initialize();
})();