4chan sounds

Play that faggy music weeb boi

当前为 2020-05-06 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         4chan sounds
// @version      0.1.5
// @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 repeatOptions = {
		all: { title: 'Repeat All', text: '[RA]' },
		one: { title: 'Repeat One', text: '[R1]' },
		none: { title: 'No Repeat', text: '[R0]' }
	};

	const Player = {
		ns: 'fc-sounds',
		sounds: [],
		container: null,
		ui: {},
		settings: {
			shuffle: false,
			repeat: Object.keys(repeatOptions)[0],
			autoshow: true,
			playlist: true,
			colors: {
				background: '#d6daf0',
				border: '#b7c5d9',
				odd_row: '#d6daf0',
				even_row: '#b7c5d9',
				expander: '#808bbf',
				expander_hover: '#9aa6e1',
				playing: '#98bff7'
			},
			allow: [
				"4cdn.org",
				"catbox.moe",
				"dmca.gripe",
				"lewd.se",
				"pomf.cat",
				"zz.ht"
			]
		},

		_templates: {
			css: ({ ns, colors }) =>
				`#${ns}-container {
					position: fixed;
					background: ${colors.background};
					border: 1px solid ${colors.border};
					display: relative;
					min-height: 200px;
					min-width: 100px;
				}
				.${ns}-show-settings .${ns}-player {
					display: none;
				}
				.${ns}-setting {
					display: none;
				}
				.${ns}-settings {
					display: none;
					padding: .25rem;
				}
				.${ns}-show-settings .${ns}-settings {
					display: block;
				}
				.${ns}-settings .${ns}-setting-header {
					font-weight: 600;
					margin-top: 0.25rem;
				}
				.${ns}-settings textarea {
					border: solid 1px ${colors.border};
					min-width: 100%;
					min-height: 4rem;
					box-sizing: border-box;
				}
				.${ns}-title {
					cursor: grab;
					text-align: center;
					border-bottom: solid 1px ${colors.border};
					padding: .25rem 0;
				}
				html.fourchan-x .${ns}-title a {
					font-size: 0;
					visibility: hidden;
					margin: 0 0.15rem;
				}
				html.fourchan-x  .${ns}-title .fa-repeat.fa-repeat-one::after {
					content: '1';
					font-size: .5rem;
					visibility: visible;
					margin-left: -1px;
				}
				.${ns}-image-link {
					text-align: center;
					display: flex;
					justify-items: center;
					justify-content: center;
					border-bottom: solid 1px ${colors.border};
				}
				.${ns}-playlist-view .${ns}-image-link {
					height: 125px !important;
				}
				.${ns}-expanded-view .${ns}-image-link {
					height: auto ;
					min-height: 125px;
				}
				.${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 {
					height: 100%;
					width: 100%;
					object-fit: contain;
				}
				.${ns}-audio {
					width: 100%;
				}
				.${ns}-list-container {
					overflow: scroll;
				}
				.${ns}-expanded-view .${ns}-list-container {
					display: none;
				}
				.${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: ${colors.playing} !important;
				}
				.${ns}-list-item:nth-child(n) {
					background: ${colors.odd_row};
				}
				.${ns}-list-item:nth-child(2n) {
					background: ${colors.even_row};
				}
				.${ns}-footer {
					padding: .15rem .25rem;
					border-top: solid 1px ${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%, ${colors.expander} 55%, ${colors.expander} 100%)
				}
				.${ns}-expander:hover {
					background: linear-gradient(to bottom right, rgba(0,0,0,0), rgba(0,0,0,0) 50%, ${colors.expander_hover} 55%, ${colors.expander_hover} 100%)
				}`,
			body: ({ ns, playlist }) =>
				`<div id="${ns}-container" class="${ns}-${!playlist ? 'expanded' : 'playlist'}-view" style="top: 100px; left: 100px; width: 350px; display: none;">
					<div class="${ns}-title">
						<span style="float: left; margin-left: 0.25rem;">
							<a class="${ns}-repeat-button fa fa-repeat" href="javascript;">RA</a>
							<a class="${ns}-shuffle-button fa fa-random" href="javascript;">Ordered</a>
							<a class="${ns}-expand-button fa fa-expand" href="javascript;">+</a>
						</span>
						4chan Sounds
						<span style="float: right; margin-right: 0.25rem;">
							<a class="${ns}-config-button fa fa-wrench" title="Settings" href="javascript;">Settings</a>
							<a class="${ns}-close-button fa fa-times" href="javascript;">X</a>
						</span>
					</div>
					<div class="${ns}-player">
						<a class="${ns}-image-link" style="height: 128px" 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" style="height: 100px">
							<ul class="${ns}-list">
							</ul>
						</div>
						<div class="${ns}-footer">
							<span class="${ns}-count">0</span> sounds
							<div class="${ns}-expander"></div>
						</div>
					</div>
					<div class="${ns}-settings">
					</div>
				</div>`,
			list: ({ ns }) =>
				Player.sounds.map(sound => `<li class="${ns}-list-item" data-id="${sound.id}">${sound.title}</li>`).join(''),
			settings: ({ ns, colors, allow, autoshow }) =>
				`<div class="${ns}-setting-header" title="Automatically show the player when the thread contains sounds.">Autoshow</div>
				<input type="checkbox" data-property="autoshow" ${autoshow ? 'checked' : ''}></input>
				<div class="${ns}-setting-header" title="Which domains sources are allowed to be loaded from.">Allow</div>
				<textarea data-property="allow" data-split="linebreak">${allow.join('\n')}</textarea>
				<div class="${ns}-setting-header">Background Color</div>
				<input type="text" data-property="colors.background" value="${colors.background}"></input>
				<div class="${ns}-setting-header">Border Color</div>
				<input type="text" data-property="colors.border" value="${colors.border}"></input>
				<div class="${ns}-setting-header">Odd Row Color</div>
				<input type="text" data-property="colors.odd_row" value="${colors.odd_row}"></input>
				<div class="${ns}-setting-header">Even Row Color</div>
				<input type="text" data-property="colors.even_row" value="${colors.even_row}"></input>
				<div class="${ns}-setting-header">Playing Row Color</div>
				<input type="text" data-property="colors.playing" value="${colors.playing}"></input>
				<div class="${ns}-setting-header">Expand Color</div>
				<input type="text" data-property="colors.expander" value="${colors.expander}"></input>
				<div class="${ns}-setting-header">Expand Hover Color</div>
				<input type="text" data-property="colors.expander_hover" value="${colors.expander_hover}"></input>`
		},

		initialize: async function () {
			await Player.loadSettings();
			Player.sounds = [ ];
			Player.playOrder = [ ];

			if (isChanX) {
				const shortcuts = document.getElementById('shortcuts');
				const showIcon = document.createElement('span');
				shortcuts.insertBefore(showIcon, document.getElementById('shortcut-settings'));

				const attrs = { id: 'shortcut-sounds', class: 'shortcut brackets-wrap', 'data-index': 0 };
				for (let attr in attrs) {
					showIcon.setAttribute(attr, attrs[attr]);
				}
				showIcon.innerHTML = '<a href="javascript:;" title="Sounds" class="fa fa-play-circle">Sounds</a>';
				showIcon.querySelector('a').addEventListener('click', Player.toggleDisplay);
			} else {
				document.querySelectorAll('#settingsWindowLink, #settingsWindowLinkBot').forEach(function (link) {
					const bracket = document.createTextNode('] [');
					const showLink = document.createElement('a');
					showLink.innerHTML = 'Sounds';
					showLink.href = 'javascript;';
					link.parentNode.insertBefore(showLink, link);
					link.parentNode.insertBefore(bracket, link);
					showLink.addEventListener('click', Player.toggleDisplay);
				});
			}

			Player.render();
		},

		_tplOptions: function () {
			return { ns: Player.ns, ...Player.settings };
		},

		render: async function () {
			if (Player.container) {
				document.body.removeChild(Player.container);
				document.head.removeChild(Player.stylesheet);
			}

			// Insert the stylesheet
			Player.stylesheet = document.createElement('style');
			Player.stylesheet.innerHTML = Player._templates.css(Player._tplOptions());
			document.head.appendChild(Player.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);
			// Keep track of various elements
			Player.ui.title = Player.container.querySelector(`.${Player.ns}-title`);
			Player.ui.closeButton = Player.container.querySelector(`.${Player.ns}-close-button`);
			Player.ui.repeatButton = Player.container.querySelector(`.${Player.ns}-repeat-button`);
			Player.ui.shuffleButton = Player.container.querySelector(`.${Player.ns}-shuffle-button`);
			Player.ui.expandButton = Player.container.querySelector(`.${Player.ns}-expand-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.settingsContainer =  Player.container.querySelector(`.${Player.ns}-settings`);
			Player.ui.count = Player.container.querySelector(`.${Player.ns}-count`);
			Player.ui.expander = Player.container.querySelector(`.${Player.ns}-expander`);
			Player.audio = Player.container.querySelector(`.${Player.ns}-audio`);

			// Render the other bits and make sure the buttons states are correct
			Player.renderList();
			Player.renderSettings();
			Player.updateRepeatButton();
			Player.updateShuffleButton();
			Player.updateExpandButton();

			// 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.closeButton.addEventListener('click', Player.hide);
			Player.ui.configButton.addEventListener('click', Player.toggleSettings);
			Player.ui.shuffleButton.addEventListener('click', Player.toggleShuffle);
			Player.ui.repeatButton.addEventListener('click', Player.toggleRepeat);
			Player.ui.expandButton.addEventListener('click', Player.togglePlaylist);

			// Add event listeners for moving/resizing
			Player.ui.expander.addEventListener('mousedown', Player.initResize, false);
			Player.ui.title.addEventListener('mousedown', Player.initMove, false);

			// Add audio event listeners
			Player.audio.addEventListener('ended', Player.next);
			Player.audio.addEventListener('pause', () => Player.ui.video.pause());
			Player.audio.addEventListener('play', () => {
				Player.ui.video.currentTime = Player.audio.currentTime;
				Player.ui.video.play();
			});
			Player.audio.addEventListener('seeked', () => Player.ui.video.currentTime = Player.audio.currentTime);
		},

		renderList: function () {
			if (Player.ui.list) {
				Player.ui.list.innerHTML = Player._templates.list(Player._tplOptions());
			}
		},

		renderSettings: function () {
			if (Player.ui.settingsContainer) {
				Player.ui.settingsContainer.innerHTML = Player._templates.settings(Player._tplOptions());
				Player.ui.settingsContainer.querySelectorAll('input, textarea').forEach(function (input) {
					input.addEventListener('blur', Player.handleSettingChange);
				});
				Player.ui.settingsContainer.querySelectorAll('input[type=checkbox]').forEach(function (input) {
					input.addEventListener('change', Player.handleSettingChange);
				});
			}
		},

		hide: function (e) {
			e && e.preventDefault();
			Player.container.style.display = 'none';
		},

		show: async function (e) {
			e && e.preventDefault();
			if (!Player.container.style.display) {
				return;
			}
			Player.container.style.display = null;
			// 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);
		},

		toggleDisplay: function (e) {
			e && e.preventDefault();
			if (Player.container.style.display === 'none') {
				Player.show();
			} else {
				Player.hide();
			}
		},

		saveSettings: function () {
			return GM.setValue(Player.ns + '.settings', JSON.stringify(Player.settings));
		},

		loadSettings: async function () {
			let settings = await GM.getValue(Player.ns + '.settings');
			if (!settings) {
				return;
			}
			try {
				settings = JSON.parse(settings);
			} catch(e) {
				return;
			}
			function _mix (to, from) {
				for (let key in from) {
					if (from[key] && typeof from[key] === 'object' && !Array.isArray(from[key])) {
						to[key] || (to[key] = {});
						_mix(to[key], from[key]);
					} else {
						to[key] = from[key];
					}
				}
			}
			_mix(Player.settings, settings);
		},

		handleSettingChange: function (e) {
			const input = e.currentTarget;
			const property = input.getAttribute('data-property').split('.');
			const split = input.getAttribute('data-split');
			const currentValue = property.reduce((v, k) => v && v[k], Player.settings);
			let newValue = input.getAttribute('type') === 'checkbox'
				? input.checked
				: input.value;
			if (split) {
				newValue = newValue.split(split === 'linebreak' ? '\n' : split);
			}
			// Not the most stringent check but enough to avoid some spamming.
			if (currentValue !== newValue) {
				// Update the setting.
				const lastProp = property.pop();
				const setOn = property.reduce((obj, k) => obj && obj[k], Player.settings);
				setOn && (setOn[lastProp] = newValue);

				// Update the stylesheet reflect any changes.
				Player.stylesheet.innerHTML = Player._templates.css(Player._tplOptions());

				// Save the new settings.
				Player.saveSettings();
			}
		},

		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;
			Player.updateShuffleButton();

			// Update the play order.
			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]];
				}
			}
			Player.saveSettings();
		},

		updateShuffleButton: function () {
			const action = Player.settings.shuffle ? 'remove' : 'add';
			Player.ui.shuffleButton.classList[action]('disabled');
			Player.ui.shuffleButton.innerHTML = Player.settings.shuffle ? '[S]' : '[O]';
			Player.ui.shuffleButton.title = Player.settings.shuffle ? 'Shuffle' : 'Ordered';
		},
		
		toggleRepeat: function (e) {
			e.preventDefault();
			const options = Object.keys(repeatOptions);
			const current = options.indexOf(Player.settings.repeat);
			Player.settings.repeat = options[(current + 4) % 3];
			Player.updateRepeatButton();
			Player.saveSettings();
		},

		updateRepeatButton: function () {
			const option = repeatOptions[Player.settings.repeat]
			Player.ui.repeatButton.innerHTML = option.text;
			Player.ui.repeatButton.title = option.title;
			const disabled = Player.settings.repeat === 'none';
			const addOne = Player.settings.repeat === 'one';
			Player.ui.repeatButton.classList[disabled ? 'add' : 'remove']('disabled');
			Player.ui.repeatButton.classList[addOne ? 'add' : 'remove']('fa-repeat-one');
		},

		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) {
			// Make sure the player isn't going off screen. 40 to give a bit of spacing for the 4chanX header.
			height = Math.min(height, document.documentElement.clientHeight - 40);

			Player.container.style.width = width + 'px';

			// Change the height of the playlist of image.
			const heightElement = Player.settings.playlist
				? Player.ui.listContainer
				: Player.ui.imageLink;

				console.log(heightElement);

			const containerHeight = parseInt(document.defaultView.getComputedStyle(Player.container).height, 10);
			const offset = containerHeight - parseInt(heightElement.style.height, 10);
			heightElement.style.height = Math.max(10, height - offset) + '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));
		},

		showThumb: function (sound) {
			Player.ui.imageLink.classList.remove(Player.ns + '-show-video');
			Player.ui.image.src = sound.thumb;
			Player.ui.imageLink.href = sound.image;
		},

		showImage: function (sound) {
			Player.ui.imageLink.classList.remove(Player.ns + '-show-video');
			Player.ui.image.src = sound.image;
			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();
		},

		hidePlaylist: function () {
			Player.settings.playlist = false;
			Player.container.classList.add(`${Player.ns}-expanded-view`);
			Player.container.classList.remove(`${Player.ns}-playlist-view`);
			Player.saveSettings();
		},

		showPlaylist: function () {
			Player.settings.playlist = true;
			Player.container.classList.remove(`${Player.ns}-expanded-view`);
			Player.container.classList.add(`${Player.ns}-playlist-view`);
			Player.saveSettings();
		},

		togglePlaylist: function (e) {
			e && e.preventDefault();
			if (Player.settings.playlist) {
				Player.hidePlaylist();
			} else {
				Player.showPlaylist();
			}
			Player.updateExpandButton();
		},

		updateExpandButton: function () {
			console.log('whut')
			if (Player.settings.playlist) {
				Player.ui.expandButton.classList.add('fa-expand');
				Player.ui.expandButton.classList.remove('fa-compress');
				Player.ui.expandButton.innerHTML = '[+]';
			} else {
				Player.ui.expandButton.classList.remove('fa-expand');
				Player.ui.expandButton.classList.add('fa-compress');
				Player.ui.expandButton.innerHTML = '[-]';
			}
		},

		add: function (title, id, src, thumb, image) {
			// Avoid duplicate additions.
			if (Player.sounds.find(sound => sound.id === id)) {
				return;
			}
			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);

			// Re-render the list.
			Player.renderList();
			this.ui.count.innerHTML = Player.sounds.length;

			// If nothing else has been added yet show the image for this sound.
			if (Player.playOrder.length === 1) {
				// If we're on a thread with autoshow enabled then make sure the player is displayed
				if (/\/thread\//.test(location.href) && Player.settings.autoshow) {
					Player.show();
				}
				Player.showThumb(sound);
			}
		},

		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.showImage(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);
		}
	};

	async function doInit() {
		await Player.initialize();

		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 (!isChanX) {
				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 Player.settings.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;
	}
})();