4chan sounds player

Play that faggy music weeb boi

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         4chan sounds player
// @version      1.0.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 ns = 'fc-sounds';

	function _logError(message, type = 'error') {
		console.error(message);
		document.dispatchEvent(new CustomEvent("CreateNotification", {
			bubbles: true,
			detail: {
				type: type,
				content: message,
				lifetime: 5
			}
		}));
	}

	function _set(object, path, value) {
		const props = path.split('.');
		const lastProp = props.pop(); 
		const setOn = props.reduce((obj, k) => obj[k] || (obj[k] = {}), object);
		setOn && (setOn[lastProp] = value);
		return object;
	}

	function _get(object, path, dflt) {
		const props = path.split('.');
		return props.reduce((obj, k) => obj && obj[k], object) || dflt;
	}
	
	function toDuration(number) {
		number = Math.floor(number || 0);
		let seconds = number % 60;
		const minutes = Math.floor(number / 60) % 60;
		const hours = Math.floor(number / 60 / 60);
		seconds < 10 && (seconds = '0' + seconds);
		return (hours ? hours + ':' : '') + minutes + ':' + seconds;
	}

	const settingsConfig = [
	{
		property: 'shuffle',
		default: false
	},
	{
		property: 'repeat',
		default: 'all'
	},
	{
		property: 'viewStyle',
		default: 'playlist'
	},
	{
		property: 'autoshow',
		default: true,
		title: 'Autoshow',
		description: 'Automatically show the player when the thread contains sounds.',
		showInSettings: true
	},
	{
		property: 'pauseOnHide',
		default: true,
		title: 'Pause on hide',
		description: 'Pause the player when it\'s hidden.',
		showInSettings: true
	},
	{
		property: 'allow',
		default: [
			'4cdn.org',
			'catbox.moe',
			'dmca.gripe',
			'lewd.se',
			'pomf.cat',
			'zz.ht'
		],
		title: 'Allow',
		description: 'Which domains sources are allowed to be loaded from.',
		showInSettings: true,
		split: '\n'
	},
	{
		property: 'colors.background',
		default: '#d6daf0',
		title: 'Background Color',
		showInSettings: true
	},
	{
		property: 'colors.border',
		default: '#b7c5d9',
		title: 'Border Color',
		showInSettings: true
	},
	{
		property: 'colors.odd_row',
		default: '#d6daf0',
		title: 'Odd Row Color',
		showInSettings: true
	},
	{
		property: 'colors.even_row',
		default: '#b7c5d9',
		title: 'Even Row Color',
		showInSettings: true
	},
	{
		property: 'colors.playing',
		default: '#98bff7',
		title: 'Playing Row Color',
		showInSettings: true
	},
	{
		property: 'colors.expander',
		default: '#808bbf',
		title: 'Expander Color',
		showInSettings: true
	}
]

	const headerOptions = {
	repeat: {
		all: { title: 'Repeat All', text: '[RA]', class: 'fa-repeat' },
		one: { title: 'Repeat One', text: '[R1]', class: 'fa-repeat fa-repeat-one' },
		none: { title: 'No Repeat', text: '[R0]', class: 'fa-repeat disabled' }
	},
	shuffle: {
		true: { title: 'Shuffled', text: '[S]', class: 'fa-random' },
		false: { title: 'Ordered', text: '[O]', class: 'fa-random disabled' },
	},
	playlist: {
		true: { title: 'Hide Playlist', text: '[+]', class: 'fa-expand' },
		false: { title: 'Show Playlist', text: '[-]', class: 'fa-compress' }
	}
}

const Player = {
	ns,

	audio: new Audio(),
	sounds: [],
	container: null,
	ui: {},
	_progressBarStyleSheets: {},
	settings: settingsConfig.reduce((settings, settingConfig) => {
		return _set(settings, settingConfig.property, settingConfig.default);
	}, {}),

	$: (...args) => Player.container.querySelector(...args),

	templates: {
		css: ({ data }) => `audio {
  width: 100%;
}

.${ns}-controls {
  align-items: center;
  padding: .5rem;
  border-bottom: solid 1px ${data.colors.border};
  background: #3f3f44;
}

.${ns}-media-control {
  height: 1.5rem;
  width: 1.5rem;
  display: flex;
  justify-content: center;
  align-items: center;
}

.${ns}-media-control > div {
  height: 1rem;
  width: .8rem;
  background: white;
}

.${ns}-media-control:hover > div {
  background: #00b6f0;
}

.${ns}-play-button-display {
  clip-path: polygon(10% 10%, 10% 90%, 35% 90%, 35% 10%, 65% 10%, 65% 90%, 90% 90%, 90% 10%, 10% 10%);
}

.${ns}-play-button-display.${ns}-play {
  clip-path: polygon(0 0, 0 100%, 100% 50%, 0 0);
}

.${ns}-previous-button-display, .${ns}-next-button-display {
  clip-path: polygon(10% 10%, 10% 90%, 30% 90%, 30% 50%, 90% 90%, 90% 10%, 30% 50%, 30% 10%, 10% 10%);
}

.${ns}-next-button-display {
  transform: scale(-1, 1);
}

.${ns}-current-time {
  color: white;
}

.${ns}-duration {
  color: #909090;
}

.${ns}-progress-bar {
  height: 1.5rem;
  display: flex;
  align-items: center;
  margin: 0 1rem;
}

.${ns}-progress-bar .${ns}-full-bar {
  height: .3rem;
  width: 100%;
  background: #131314;
  border-radius: 1rem;
  position: relative;
}

.${ns}-progress-bar .${ns}-full-bar > div {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: 1rem;
}

.${ns}-progress-bar .${ns}-full-bar .${ns}-loaded-bar {
  background: #5a5a5b;
}

.${ns}-progress-bar .${ns}-full-bar .${ns}-current-bar {
  display: flex;
  justify-content: end;
  align-items: center;
}

.${ns}-progress-bar .${ns}-full-bar .${ns}-current-bar:after {
  content: '';
  background: white;
  height: .8rem;
  min-width: .8rem;
  border-radius: 1rem;
  box-shadow: rgba(0, 0, 0, 0.76) 0 0 3px 0;
}

.${ns}-progress-bar:hover .${ns}-current-bar:after {
  background: #00b6f0;
}

.${ns}-seek-bar .${ns}-current-bar {
  background: #00b6f0;
}

.${ns}-volume-bar .${ns}-current-bar {
  background: white;
}

.${ns}-volume-bar {
  width: 3.5rem;
}

.${ns}-expander {
  position: absolute;
  bottom: 0px;
  right: 0px;
  height: .75rem;
  width: .75rem;
  cursor: se-resize;
  background: linear-gradient(to bottom right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 50%, ${data.colors.expander} 55%, ${data.colors.expander} 100%);
}

.${ns}-footer {
  padding: .15rem .25rem;
  border-top: solid 1px ${data.colors.border};
}

.${ns}-title {
  cursor: grab;
  text-align: center;
  border-bottom: solid 1px ${data.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 ${data.colors.border};
}

.${ns}-image-link .${ns}-video {
  display: none;
}

.${ns}-image, .${ns}-video {
  height: 100%;
  width: 100%;
  object-fit: contain;
}

.${ns}-image-link.${ns}-show-video .${ns}-video {
  display: block;
}

.${ns}-image-link.${ns}-show-video .${ns}-image {
  display: none;
}

#${ns}-container {
  position: fixed;
  background: ${data.colors.background};
  border: 1px solid ${data.colors.border};
  display: relative;
  min-height: 200px;
  min-width: 100px;
}

.${ns}-row {
  display: flex;
  flex-wrap: wrap;
}

.${ns}-col-auto {
  flex: 0 0 auto;
  width: auto;
  max-width: 100%;
  margin-left: 0.25rem;
}

.${ns}-col {
  flex-basis: 0;
  flex-grow: 1;
  max-width: 100%;
  width: 100%;
}

.${ns}-list-container {
  overflow: auto;
}

.${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;
  background: ${data.colors.odd_row};
}

.${ns}-list-item.playing {
  background: ${data.colors.playing} !important;
}

.${ns}-list-item:nth-child(2n) {
  background: ${data.colors.even_row};
}

.${ns}-settings {
  display: none;
  padding: .25rem;
}

.${ns}-settings .${ns}-setting-header {
  font-weight: 600;
  margin-top: 0.25rem;
}

.${ns}-settings textarea {
  border: solid 1px ${data.colors.border};
  min-width: 100%;
  min-height: 4rem;
  box-sizing: border-box;
}

#${ns}-container[data-view-style="settings"] .${ns}-player {
  display: none;
}

#${ns}-container[data-view-style="settings"] .${ns}-settings {
  display: block;
}

#${ns}-container[data-view-style="image"] .${ns}-list-container {
  display: none;
}

#${ns}-container[data-view-style="playlist"] .${ns}-image-link {
  height: 125px !important;
}

#${ns}-container[data-view-style="image"] .${ns}-image-link {
  height: auto;
  min-height: 125px;
}
`,
		body: ({ data }) => `<div id="${ns}-container" data-view-style="${data.viewStyle}" style="top: 100px; left: 100px; width: 350px; display: none;">
	<div class="${ns}-title ${ns}-row" style="justify-content: between;">
		${Player.templates.header({ data })}
	</div>
	<div class="${ns}-player">
		${Player.templates.player({ data })}
	</div>
	<div class="${ns}-settings">
		${Player.templates.settings({ data })}
	</div>
</div>`,
		header: ({ data }) => `<div class="${ns}-col-auto" style="margin-left: 0.25rem;">`
	+ Object.keys(headerOptions).map(key => {
		let option = headerOptions[key][data[key]] || headerOptions[key][Object.keys(headerOptions[key])[0]];
		return `<a class="${ns}-${key}-button fa ${option.class}" title="${option.title}" href="javascript;">
			${option.text}
		</a>`
	}).join('') + `
</div><div class="${ns}-col" style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">
	${Player.playing ? Player.playing.title : '4chan Sounds'}
</div>
<div class=".${ns}-col-auto" style="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>
</div>`,
		player: ({ data }) => `<a class="${ns}-image-link" style="height: 128px" target="_blank">
	<img class="${ns}-image"></img>
	<video class="${ns}-video"></video>
</a>
<div class="${ns}-controls ${ns}-row">
	${Player.templates.controls({ data })}
</div>
<div class="${ns}-list-container" style="height: 100px">
	<ul class="${ns}-list">
		${Player.templates.list({ data })}
	</ul>
</div>
<div class="${ns}-footer">
	<span class="${ns}-count">0</span> sounds
	<div class="${ns}-expander"></div>
</div>`,
		controls: ({ data }) => `<div class="${ns}-col-auto ${ns}-row" href="javascript;">
	<div class="${ns}-media-control ${ns}-previous-button">
		<div class="${ns}-previous-button-display"></div>
	</div>
	<div class="${ns}-media-control ${ns}-play-button">
		<div class="${ns}-play-button-display ${!Player.audio || Player.audio.paused ? `${ns}-play` : ''}"></div>
	</div>
	<div class="${ns}-media-control ${ns}-next-button">
		<div class="${ns}-next-button-display"></div>
	</div>
</div>
<div class="${ns}-col">
	<div class="${ns}-seek-bar ${ns}-progress-bar">
		<div class="${ns}-full-bar">
			<div class="${ns}-loaded-bar"></div>
			<div class="${ns}-current-bar"></div>
		</div>
	</div>
</div>
<div class="${ns}-col-auto">
	<span class="${ns}-current-time">0:00</span><span class="${ns}-duration"> / 0:00</span>
</div>
<div class="${ns}-col-auto">
	<div class="${ns}-volume-bar ${ns}-progress-bar">
		<div class="${ns}-full-bar">
			<div class="${ns}-current-bar" style="width: ${Player.audio.volume * 100}%"></div>
		</div>
	</div>
</div>`,
		list: ({ data }) => Player.sounds.map(sound =>
	`<li class="${ns}-list-item ${sound.playing ? 'playing' : ''}" data-id="${sound.id}">
		${sound.title}
	</li>`
).join(''),
		settings: ({ data }) => 
settingsConfig.filter(setting => setting.showInSettings).map(setting => {
	let out = `<div class="${ns}-setting-header" ${setting.title ? `title="${setting.desc}"` : ''}>${setting.title}</div>`;
	if (typeof setting.default === 'boolean') {
		out += `<input type="checkbox" data-property="${setting.property}" ${_get(data, setting.property, false) ? 'checked' : ''}></input>`;
	} else if (Array.isArray(setting.default)) {
		out += `<textarea data-property="${setting.property}">${_get(data, setting.property, '').join(setting.split)}</textarea>`;
	} else {
		out += `<input type="text" data-property="${setting.property}" value="${_get(data, setting.property, '')}"></input>`;
	}
	return out;
}).join('')
	},

	delegatedEvents: {
		click: {
			[`.${ns}-close-button`]: 'hide',

			// Playback settings
			[`.${ns}-shuffle-button`]: 'toggleShuffle',
			[`.${ns}-repeat-button`]: 'toggleRepeat',

			// Media controls
			[`.${ns}-previous-button`]: 'previous',
			[`.${ns}-play-button`]: 'togglePlay',
			[`.${ns}-next-button`]: 'next',
			[`.${ns}-seek-bar`]: 'handleSeek',
			[`.${ns}-volume-bar`]: 'handleVolume',

			// View settings
			[`.${ns}-playlist-button`]: 'togglePlayerView',
			[`.${ns}-config-button`]: 'toggleSettings',

			// Playlist controls
			[`.${ns}-list`]: 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);
			}
		},
		mousedown: {
			[`.${ns}-title`]: 'initMove',
			[`.${ns}-expander`]: 'initResize',
			[`.${ns}-seek-bar`]: () => Player._seekBarDown = true,
			[`.${ns}-volume-bar`]: () => Player._volumeBarDown = true
		},
		mousemove: {
			[`.${ns}-seek-bar`]: e => Player._seekBarDown && Player.handleSeek(e),
			[`.${ns}-volume-bar`]: e => Player._volumeBarDown && Player.handleVolume(e)
		},
		focusout: {
			[`.${ns}-settings input, .${ns}-settings textarea`]: 'handleSettingChange'
		},
		change: {
			[`.${ns}-settings input[type=checkbox]`]: 'handleSettingChange'
		}
	},

	undelegatedEvents: {
		mouseleave: {
			[`.${ns}-seek-bar`]: e => Player._seekBarDown && Player.handleSeek(e),
			[`.${ns}-volume-bar`]: e => Player._volumeBarDown && Player.handleVolume(e)
		},
		mouseup: {
			body: () => {
				Player._seekBarDown = false;
				Player._volumeBarDown = false;
			}
		},
		play: { [`.${ns}-video`]: 'syncVideo' },
		playing: { [`.${ns}-video`]: 'syncVideo' },
		pause: { [`.${ns}-video`]: 'syncVideo' },
		seeked: { [`.${ns}-video`]: 'syncVideo' },
		loadeddata: { [`.${ns}-video`]: 'syncVideo' }
	},

	audioEvents: {
		ended: 'next',
		pause:'handleAudioEvent',
		play: 'handleAudioEvent',
		seeked: 'handleAudioEvent',
		waiting: 'handleAudioEvent',
		timeupdate: 'updateDuration',
		loadedmetadata: 'updateDuration',
		durationchange: 'updateDuration',
		volumechange: 'updateVolume',
		loadstart: 'pollForLoading'
	},

	/**
	 * Returns the function of Player referenced by name or a given handler function.
	 * @param {String|Function} handler Name to function on Player or a handler function.
	 */
	getEventHandler: function (handler) {
		return typeof handler === 'string' ? Player[handler] : handler;
	},

	/**
	 * Set up the player.
	 */
	initialize: async function () {
		try {
			await Player.loadSettings();
			Player.sounds = [ ];
			Player.playOrder = [ ];
 
			// If it's already known that 4chan X is running then setup the button for it.
			// If not add the the [Sounds] link in the top and bottom nav.
			if (isChanX) {
				Player.initChanX()
			} 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);
				});
			}

			// Render the player, but not neccessarily show it.
			Player.render();
		} catch (err) {
			_logError('There was an error intiaizing the sound player. Please check the console for details.');
			console.error('[4chan sounds player]', err);
			// Can't recover so throw this error.
			throw err;
		}
	},

	/**
	 * Create the player show/hide button in to the 4chan X header.
	 */
	initChanX: function () {
		if (Player._initedChanX) {
			return;
		}
		Player._initedChanX = true;
		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);
	},

	/**
	 * Persist the player settings.
	 */
	saveSettings: function () {
		try {
			return GM.setValue(ns + '.settings', JSON.stringify(Player.settings));
		} catch (err) {
			_logError('There was an error saving the sound player settings. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},

	/**
	 * Restore the saved player settings.
	 */
	loadSettings: async function () {
		try {
			let settings = await GM.getValue(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);
		} catch (err) {
			_logError('There was an error loading the sound player settings. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},

	/**
	 * Generate the data passed to the templates.
	 */
	_tplOptions: function () {
		return { data: Player.settings };
	},

	/**
	 * Render the player.
	 */
	render: async function () {
		try {
			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(`#${ns}-container`);
			document.body.appendChild(Player.container);

			// Keep track of heavily updated elements.
			Player.ui.currentTime = Player.$(`.${ns}-current-time`);
			Player.ui.duration = Player.$(`.${ns}-duration`);
			Player.ui.currentTimeBar = Player.$(`.${ns}-seek-bar .${ns}-current-bar`);
			Player.ui.loadedBar = Player.$(`.${ns}-seek-bar .${ns}-loaded-bar`);

			// Add stylesheets to adjust the progress indicator of the seekbar and volume bar.
			document.head.appendChild(Player._progressBarStyleSheets[`.${ns}-seek-bar`] = document.createElement('style'));
			document.head.appendChild(Player._progressBarStyleSheets[`.${ns}-volume-bar`] = document.createElement('style'));
			Player.updateDuration();
			Player.updateVolume();

			// Wire up delegated events on the container.
			for (let evt in Player.delegatedEvents) {
				Player.container.addEventListener(evt, function (e) {
					for (let selector in Player.delegatedEvents[evt]) {
						const eventTarget = e.target.closest(selector);
						if (eventTarget) {
							e.eventTarget = eventTarget;
							return Player.getEventHandler(Player.delegatedEvents[evt][selector])(e);
						}
					}
				});
			}

			// Wire up undelegated events.
			Player.wireUpUndelegatedEvents();

			// Wite up audio events.
			for (let evt in Player.audioEvents) {
				Player.audio.addEventListener(evt, Player.getEventHandler(Player.audioEvents[evt]));
			}
		} catch (err) {
			_logError('There was an error rendering the sound player. Please check the console for details.');
			console.error('[4chan sounds player]', err);
			// Can't recover, throw.
			throw err;
		}
	},

	/**
	 * Render the player header.
	 */
	renderHeader: function () {
		if (!Player.container) {
			return;
		}
		Player.$(`.${ns}-title`).innerHTML = Player.templates.header(Player._tplOptions());
	},

	/**
	 * Render the playlist.
	 */
	renderList: function () {
		if (!Player.container) {
			return;
		}
		if (Player.$(`.${ns}-list`)) {
			Player.$(`.${ns}-list`).innerHTML = Player.templates.list(Player._tplOptions());
		}
	},

	/**
	 * Set, or reset, directly bound events.
	 */
	wireUpUndelegatedEvents: function () {
		for (let evt in Player.undelegatedEvents) {
			for (let selector in Player.undelegatedEvents[evt]) {
				document.querySelectorAll(selector).forEach(element => {
					const handler = Player.getEventHandler(Player.undelegatedEvents[evt][selector]);
					element.removeEventListener(evt, handler);
					element.addEventListener(evt, handler);
				});
			}
		}
	},

	/**
	 * Handle audio events. Sync the video up, and update the controls.
	 */
	handleAudioEvent: function () {
		Player.syncVideo();
		Player.updateDuration();
		Player.$(`.${ns}-play-button .${ns}-play-button-display`).classList[Player.audio.paused ? 'add' : 'remove'](`${ns}-play`);
	},

	/**
	 * Sync the webm to the audio. Matches the videos time and play state to the audios.
	 */
	syncVideo: function () {
		const paused = Player.audio.paused;
		const video = Player.$(`.${ns}-video`);
		if (video) {
			video.currentTime = Player.audio.currentTime;
			if (paused) {
				video.pause();
			} else {
				video.play();
			}
		}
	},

	/**
	 * Poll for how much has loaded. I know there's the progress event but it unreliable.
	 */
	pollForLoading: function () {
		Player._loadingPoll = Player._loadingPoll || setInterval(Player.updateLoaded, 1000);
	},

	/**
	 * Stop polling for how much has loaded.
	 */
	stopPollingForLoading: function () {
		Player._loadingPoll && clearInterval(Player._loadingPoll);
		Player._loadingPoll = null;
	},

	/**
	 * Update the loading bar.
	 */
	updateLoaded: function () {
		const length = Player.audio.buffered.length;
		const size = length > 0
			? (Player.audio.buffered.end(length - 1) / Player.audio.duration) * 100
			: 0;
		// If it's fully loaded then stop polling.
		size === 100 && Player.stopPollingForLoading();
		Player.ui.loadedBar.style.width = size + '%';
	},

	/**
	 * Update the seek bar and the duration labels.
	 */
	updateDuration: function () {
		if (!Player.container) {
			return;
		}
		Player.ui.currentTime.innerHTML = toDuration(Player.audio.currentTime);
		Player.ui.duration.innerHTML = ' / ' + toDuration(Player.audio.duration);
		Player.updateProgressBarPosition(`.${ns}-seek-bar`, Player.ui.currentTimeBar, Player.audio.currentTime, Player.audio.duration);
	},

	/**
	 * Update the volume bar.
	 */
	updateVolume: function () {
		Player.updateProgressBarPosition(`.${ns}-volume-bar`, Player.$(`.${ns}-volume-bar .${ns}-current-bar`), Player.audio.volume, 1);
	},

	/**
	 * Update a progress bar width. Adjust the margin of the circle so it's contained within the bar at both ends.
	 */
	updateProgressBarPosition: function (id, bar, current, total) {
		current || (current = 0);
		total || (total = 0);
		const ratio = !total ? 0 : Math.max(0, Math.min(((current || 0) / total), 1));
		bar.style.width = (ratio * 100) + '%';
		if (Player._progressBarStyleSheets[id]) {
			Player._progressBarStyleSheets[id].innerHTML = `${id} .${ns}-current-bar:after {
				margin-right: ${-.8 * (1 - ratio)}rem;
			}`;
		}
	},

	/**
	 * Handle the user interacting with the seek bar.
	 */
	handleSeek: function (e) {
		e.preventDefault();
		if (Player.container && Player.audio.duration && Player.audio.duration !== Infinity) {
			const ratio = e.offsetX / parseInt(document.defaultView.getComputedStyle(e.eventTarget || e.target).width, 10);
			Player.audio.currentTime = Player.audio.duration * ratio;
		}
	},

	/**
	 * Handle the user interacting with the volume bar.
	 */
	handleVolume: function (e) {
		e.preventDefault();
		if (!Player.container) {
			return;
		}
		const ratio = e.offsetX / parseInt(document.defaultView.getComputedStyle(e.eventTarget || e.target).width, 10);
		Player.audio.volume = Math.max(0, Math.min(ratio, 1));
		Player.updateVolume();
	},

	/**
	 * Change what view is being shown
	 * @param {Chagn} e 
	 */
	setViewStyle: function (style) {
		Player.settings.viewStyle = style;
		Player.container.setAttribute('data-view-style', style);
	},

	/**
	 * Togle the display status of the player.
	 */
	toggleDisplay: function (e) {
		e && e.preventDefault();
		if (Player.container.style.display === 'none') {
			Player.show();
		} else {
			Player.hide();
		}
	},

	/**
	 * Hide the player. Stops polling for changes, and pauses the aduio if set to.
	 */
	hide: function (e) {
		if (!Player.container) {
			return;
		}
		try {
			e && e.preventDefault();
			Player._hiddenWhilePolling = !!Player._loadingPoll;
			Player.stopPollingForLoading();
			Player.container.style.display = 'none';
			if (Player.settings.pauseOnHide) {
				Player.pause();
			}
		} catch (err) {
			_logError('There was an error hiding the sound player. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},

	/**
	 * Show the player. Reapplies the saved position/size, and resumes loadeing polling if it was paused.
	 * @param {*} e 
	 */
	show: async function (e) {
		if (!Player.container) {
			return;
		}
		try {
			e && e.preventDefault();
			if (!Player.container.style.display) {
				return;
			}
			Player._hiddenWhilePolling && Player.pollForLoading();
			Player.container.style.display = null;
			// Apply the last position/size
			const [ top, left ] = (await GM.getValue(ns + '.position') || '').split(':');
			const [ width, height ] = (await GM.getValue(ns + '.size') || '').split(':');
			+width && +height && Player.resizeTo(width, height);
			+top && +left && Player.moveTo(top, left);
		} catch (err) {
			_logError('There was an error showing the sound player. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},
	
	/**
	 * Toggle the repeat style.
	 */
	toggleRepeat: function (e) {
		try {
			e.preventDefault();
			const options = Object.keys(headerOptions.repeat);
			const current = options.indexOf(Player.settings.repeat);
			Player.settings.repeat = options[(current + 4) % 3];
			Player.renderHeader();
			Player.saveSettings();
		} catch (err) {
			_logError('There was an error changing the repeat setting. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},
	
	
	/**
	 * Toggle the shuffle style.
	 */
	toggleShuffle: function (e) {
		try {
			e.preventDefault();
			Player.settings.shuffle = !Player.settings.shuffle;
			Player.renderHeader();

			// 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();
		} catch (err) {
			_logError('There was an error changing the shuffle setting. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},

	
	/**
	 * Toggle whether the player or settings are displayed.
	 */
	toggleSettings: function (e) {
		try {
			e.preventDefault();
			if (Player.settings.viewStyle === 'settings') {
				Player.setViewStyle(Player._preSettingsView);
			} else {
				Player._preSettingsView = Player.settings.viewStyle;
				Player.setViewStyle('settings');
			}
		} catch (err) {
			_logError('There was an error rendering the sound player settings. Please check the console for details.');
			console.error('[4chan sounds player]', err);
			// Can't recover, throw.
			throw err;
		}
	},

	/**
	 * Handle the user making a change in the settings view.
	 */
	handleSettingChange: function (e) {
		try {
			const input = e.eventTarget;
			const property = input.getAttribute('data-property');
			const settingConfig = settingsConfig.find(settingConfig => settingConfig.property === property);
			const currentValue = _get(Player.settings, property);
			let newValue = input[input.getAttribute('type') === 'checkbox' ? 'checked' : 'value'];
			if (settingConfig && settingConfig.split) {
				newValue = newValue.split(decodeURIComponent(settingConfig.split));
			}
			// Not the most stringent check but enough to avoid some spamming.
			if (currentValue !== newValue) {
				// Update the setting.
				_set(Player.settings, property, newValue);

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

				// Save the new settings.
				Player.saveSettings();
			}
		} catch (err) {
			_logError('There was an updating the setting. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},

	/**
	 * Handle the user grabbing the expander.
	 */
	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);
	},

	/**
	 * Handle the user dragging the expander.
	 */
	doResize: function(e) {
		e.preventDefault();
		Player.resizeTo(Player._startWidth + e.clientX - Player._startX, Player._startHeight + e.clientY - Player._startY);
	},

	/**
	 * Handle the user releasing the expander.
	 */
	stopResize: function() {
		const style = document.defaultView.getComputedStyle(Player.container);
		document.documentElement.removeEventListener('mousemove', Player.doResize, false);
		document.documentElement.removeEventListener('mouseup', Player.stopResize, false);
		enableUserSelect();
		GM.setValue(ns + '.size', parseInt(style.width, 10) + ':' + parseInt(style.height, 10));
	},

	/**
	 * Resize the player.
	 */
	resizeTo: function (width, height) {
		if (!Player.container) {
			return;
		}
		// 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 or image.
		const heightElement = Player.settings.viewStyle === 'playlist'
			? Player.$(`.${ns}-list-container`)
			: Player.$(`.${ns}-image-link`);

		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';
	},

	/**
	 * Handle the user grabbing the header.
	 */
	initMove: function (e) {
		disableUserSelect();
		Player.$(`.${ns}-title`).style.cursor = 'grabbing';

		// Try to reapply the current sizing to fix oversized winows.
		const style = document.defaultView.getComputedStyle(Player.container);
		Player.resizeTo(parseInt(style.width, 10), parseInt(style.height, 10));

		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);
	},

	/**
	 * Handle the user dragging the header.
	 */
	doMove: function (e) {
		e.preventDefault();
		Player.moveTo(e.clientX - Player._offsetX, e.clientY - Player._offsetY);
	},

	/**
	 * Handle the user releasing the heaer.
	 */
	stopMove: function (e) {
		document.documentElement.removeEventListener('mousemove', Player.doMove, false);
		document.documentElement.removeEventListener('mouseup', Player.stopMove, false);
		Player.$(`.${ns}-title`).style.cursor = null;
		enableUserSelect();
		GM.setValue(ns + '.position', parseInt(Player.container.style.left, 10) + ':' + parseInt(Player.container.style.top, 10));
	},

	/**
	 * Move the player.
	 */
	moveTo: function (x, y) {
		if (!Player.container) {
			return;
		}
		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';
	},

	/**
	 * Update the image displayed in the player.
	 */
	showImage: function (sound, thumb) {
		if (!Player.container) {
			return;
		}
		try {
			Player.$(`.${ns}-image`).src = thumb ? sound.image : sound.thumb;
			Player.$(`.${ns}-image-link`).href = sound.image;
			Player.$(`.${ns}-image-link`).classList.remove(ns + '-show-video');
		} catch (err) {
			_logError('There was an error display the sound player image. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},

	/**
	 * Play the video for a sound in place of an image.
	 */
	playVideo: function (sound) {
		if (!Player.container) {
			return;
		}
		try {
			Player.$(`.${ns}-video`).src = sound.image;
			Player.$(`.${ns}-image-link`).href = sound.image;
			Player.$(`.${ns}-image-link`).classList.add(ns + '-show-video');
		} catch (err) {
			_logError('There was an error display the sound player image. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},

	/**
	 * Switch between playlist and image view.
	 */
	togglePlayerView: function (e) {
		e.preventDefault()
		if (!Player.container) {
			return;
		}
		e && e.preventDefault();
		let style = Player.settings.viewStyle === 'playlist' ? 'image' : 'playlist';
		try {
			Player.setViewStyle(style);
			Player.renderHeader();
			Player.saveSettings();
		} catch (err) {
			_logError('There was an error switching the view style. Please check the console for details.', 'warning');
			console.error('[4chan sounds player]', err);
		}
	},

	/**
	 * Add a new sound from the thread to the player.
	 */
	add: function (title, id, src, thumb, image) {
		try {
			// 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);

			if (Player.container) {
				// Re-render the list.
				Player.renderList();
				Player.$(`.${ns}-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.showImage(sound);
				}
			}
		} catch (err) {
			_logError('There was an error adding to the sound player. Please check the console for details.');
			console.log('[4chan sounds player', title, id, src, thumb, image);
			console.error('[4chan sounds player]', err);
		}
	},

	/**
	 * Start playback.
	 */
	play: function (sound) {
		if (!Player.audio) {
			return;
		}

		try {
			// If a new sound is being played update the display.
			if (sound) {
				if (Player.playing) {
					Player.playing.playing = false;
				}
				sound.playing = true;
				Player.playing = sound;
				Player.renderHeader();
				Player.audio.src = sound.src;
				if (sound.image.endsWith('.webm')) {
					Player.playVideo(sound);
				} else {
					Player.showImage(sound);
				}
				Player.renderList();
			}
			Player.audio.play();
		} catch (err) {
			_logError('There was an error playing the sound. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},

	/**
	 * Pause playback.
	 */
	pause: function () {
		Player.audio && Player.audio.pause();
	},

	/**
	 * Switching being playing and paused.
	 */
	togglePlay: function () {
		if (Player.audio.paused) {
			Player.play();
		} else {
			Player.pause();
		}
	},

	/**
	 * Play the next sound.
	 */
	next: function () {
		Player._movePlaying(1);
	},

	/**
	 * Play the previous sound.
	 */
	previous: function () {
		Player._movePlaying(-1);
	},

	_movePlaying: function (direction) {
		if (!Player.audio) {
			return;
		}
		try {
			// 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);
		} catch (err) {
			_logError(`There was an error selecting the ${direction > 0 ? 'next': 'previous'} track. Please check the console for details.`);
			console.error('[4chan sounds player]', err);
		}
	}
};


	document.addEventListener('DOMContentLoaded', async function() {
		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('4chanXInitFinished', function () {
		isChanX = true;
		Player.initChanX();
	});

	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) {
		try {
			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);
				}
			}
		} catch (err) {
			_logError('There was an issue parsing the files. Please check the console for details.');
			console.log('[4chan sounds player]', post)
			console.error(err);
		}
	};

	function disableUserSelect () {
		document.body.style.userSelect = 'none';
		document.body.style.MozUserSelect = 'none';
	}

	function enableUserSelect () {
		document.body.style.userSelect = null;
		document.body.style.MozUserSelect = null;
	}
})();