4chan sounds

Play that faggy music weeb boi

目前為 2020-05-05 提交的版本,檢視 最新版本

// ==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();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址