4chan sounds player

Play that faggy music weeb boi

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         4chan sounds player
// @version      1.5.2
// @namespace    rccom
// @description  Play that faggy music weeb boi
// @author       RCC
// @match        *://boards.4chan.org/*
// @match        *://boards.4channel.org/*
// @match        *://desuarchive.org/*
// @match        *://arch.b4k.co/*
// @match        *://archived.moe/*
// @grant        GM.getValue
// @grant        GM.setValue
// @run-at       document-start
// ==/UserScript==


/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = "./src/main.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./src/components/controls.js":
/*!************************************!*\
  !*** ./src/components/controls.js ***!
  \************************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = {
	atRoot: [ 'togglePlay', 'play', 'pause', 'next', 'previous' ],

	delegatedEvents: {
		click: {
			[`.${ns}-previous-button`]: () => Player.previous(),
			[`.${ns}-play-button`]: 'togglePlay',
			[`.${ns}-next-button`]: () => Player.next(),
			[`.${ns}-seek-bar`]: 'controls.handleSeek',
			[`.${ns}-volume-bar`]: 'controls.handleVolume',
		},
		mousedown: {
			[`.${ns}-seek-bar`]: () => Player._seekBarDown = true,
			[`.${ns}-volume-bar`]: () => Player._volumeBarDown = true
		},
		mousemove: {
			[`.${ns}-seek-bar`]: e => Player._seekBarDown && Player.controls.handleSeek(e),
			[`.${ns}-volume-bar`]: e => Player._volumeBarDown && Player.controls.handleVolume(e)
		}
	},

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

	audioEvents: {
		ended: () => Player.next(),
		pause: 'controls.handleAudioEvent',
		play: 'controls.handleAudioEvent',
		seeked: 'controls.handleAudioEvent',
		waiting: 'controls.handleAudioEvent',
		timeupdate: 'controls.updateDuration',
		loadedmetadata: 'controls.updateDuration',
		durationchange: 'controls.updateDuration',
		volumechange: 'controls.updateVolume',
		loadstart: 'controls.pollForLoading'
	},

	initialize: function () {
		Player.on('order', () => Player.currentIndex = Player.sounds.indexOf(Player.playing) + 1);
		Player.on('show', () => Player._hiddenWhilePolling && Player.controls.pollForLoading());
		Player.on('hide', () => {
			Player._hiddenWhilePolling = !!Player._loadingPoll;
			Player.controls.stopPollingForLoading();
		});
	},

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

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

		try {
			// If nothing is currently selected to play start playing the first sound.
			if (!sound && !Player.playing && Player.sounds.length) {
				sound = Player.sounds[0];
			}
			// 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.audio.src = sound.src;
				Player.currentIndex = Player.sounds.indexOf(sound) + 1;
				Player.trigger('playsound', sound);
			}
			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 (force) {
		Player.audio && Player.audio.pause();
	},

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

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

	_movePlaying: function (direction, force) {
		if (!Player.audio) {
			return;
		}
		try {
			// If there's no sound fall out.
			if (!Player.sounds.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.sounds.indexOf(Player.playing);
			if (currentIndex === -1) {
				return Player.play(Player.sounds[0]);
			}
			// Get the next index, either repeating the same, wrapping round to repeat all or just moving the index.
			const nextIndex = !force && Player.config.repeat === 'one'
				? currentIndex
				: Player.config.repeat === 'all'
					? ((currentIndex + direction) + Player.sounds.length) % Player.sounds.length
					: currentIndex + direction;
			const nextSound = Player.sounds[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);
		}
	},

	/**
	 * Handle audio events. Sync the video up, and update the controls.
	 */
	handleAudioEvent: function () {
		Player.controls.syncVideo();
		Player.controls.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 () {
		if (Player.playlist.isVideo) {
			const paused = Player.audio.paused;
			const video = Player.$(`.${ns}-video`);
			if (video) {
				if (paused) {
					video.currentTime = Math.min(Player.audio.currentTime, video.duration);
					video.pause();
				} else if (Player.audio.currentTime < video.duration) {
					video.currentTime = Player.audio.currentTime;
					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.controls.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.controls.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.controls.updateProgressBarPosition(`.${ns}-seek-bar`, Player.ui.currentTimeBar, Player.audio.currentTime, Player.audio.duration);
	},

	/**
	 * Update the volume bar.
	 */
	updateVolume: function () {
		Player.controls.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.controls.updateVolume();
	}
}


/***/ }),

/***/ "./src/components/display.js":
/*!***********************************!*\
  !*** ./src/components/display.js ***!
  \***********************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = {
	atRoot: [ 'show', 'hide' ],

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

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

	/**
	 * Render the player.
	 */
	render: async function () {
		try {
			if (Player.container) {
				document.body.removeChild(Player.container);
				document.head.removeChild(Player.stylesheet);
			}

			Player.display.updateStylesheet();

			// Create the main player.
			const el = document.createElement('div');
			el.innerHTML = Player.templates.body();
			Player.container = el.querySelector(`#${ns}-container`);
			document.body.appendChild(Player.container);

			Player.trigger('rendered');

			// 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.controls.updateDuration();
			Player.controls.updateVolume();
		} 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;
		}
	},

	updateStylesheet: function () {
		// Insert the stylesheet if it doesn't exist.
		if (!Player.stylesheet) {
			Player.stylesheet = document.createElement('style');
			document.head.appendChild(Player.stylesheet);
		}

		Player.stylesheet.innerHTML = Player.templates.css();
	},

	/**
	 * Change what view is being shown
	 */
	setViewStyle: function (style) {
		// Get the size prior to switching.
		const { width, height } = Player.container.getBoundingClientRect();

		// Change the style.
		Player.config.viewStyle = style;
		Player.container.setAttribute('data-view-style', style);

		// Try to reapply the pre change sizing.
		Player.position.resize(parseInt(width, 10), parseInt(height, 10));
	},

	/**
	 * Togle the display status of the player.
	 */
	toggle: 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.container.style.display = 'none';

			Player.isHidden = true;
			Player.trigger('hide');
		} 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.container.style.display = null;

			Player.isHidden = false;
			Player.trigger('show');
		} catch (err) {
			_logError('There was an error showing the sound player. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	}
}


/***/ }),

/***/ "./src/components/events.js":
/*!**********************************!*\
  !*** ./src/components/events.js ***!
  \**********************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = {
	atRoot: [ 'on', 'off', 'trigger' ],

	// Holder of event handlers.
	_events: { },
	_delegatedEvents: { },
	_undelegatedEvents: { },
	_audioEvents: [ ],

	initialize: function () {
		const eventLocations = { Player, ...Player.components };
		const delegated = Player.events._delegatedEvents;
		const undelegated = Player.events._undelegatedEvents;
		const audio = Player.events._audioEvents;

		for (name in eventLocations) {
			const comp = eventLocations[name];
			for (let evt in comp.delegatedEvents || {}) {
				delegated[evt] || (delegated[evt] = [])
				delegated[evt].push(comp.delegatedEvents[evt]);
			}
			for (let evt in comp.undelegatedEvents || {}) {
				undelegated[evt] || (undelegated[evt] = [])
				undelegated[evt].push(comp.undelegatedEvents[evt]);
			}
			comp.audioEvents && (audio.push(comp.audioEvents));
		}
	
		Player.on('rendered', function () {
			// Wire up delegated events on the container.
			for (let evt in delegated) {
				Player.container.addEventListener(evt, function (e) {
					let nodes = [ e.target ];
					while (nodes[nodes.length - 1] !== Player.container) {
						nodes.push(nodes[nodes.length - 1].parentNode);
					}
					for (let node of nodes) {
						for (let eventList of delegated[evt]) {
							for (let selector in eventList) {
								if (node.matches && node.matches(selector)) {
									e.eventTarget = node;
									let handler = Player.events.getHandler(eventList[selector]);
									// If the handler returns false stop propogation
									if (handler && handler(e) === false) {
										return;
									}
								}
							}
						}
					}
				});
			}

			// Wire up undelegated events.
			Player.events.addUndelegatedListeners(Player.events._undelegatedEvents);

			// Wire up audio events.
			for (let eventList of audio) {
				for (let evt in eventList) {
					Player.audio.addEventListener(evt, Player.events.getHandler(eventList[evt]));
				}
			}
		});
	},

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

	/**
	 * Create an event listener on the player.
	 *
	 * @param {String} evt The name of the events.
	 * @param {function} handler The handler function.
	 */
	on: function (evt, handler) {
		Player.events._events[evt] || (Player.events._events[evt] = []);
		Player.events._events[evt].push(handler);
	},

	/**
	 * Remove an event listener on the player.
	 *
	 * @param {String} evt The name of the events.
	 * @param {function} handler The handler function.
	 */
	off: function (evt, handler) {
		const index = Player.events._events[evt] && Player.events._events[evt].indexOf(handler);
		if (index > -1) {
			Player.events._events[evt].splice(index, 1);
		}
	},

	/**
	 * Trigger an event on the player.
	 *
	 * @param {String} evt The name of the events.
	 * @param {*} data Data passed to the handler.
	 */
	trigger: async function (evt, ...data) {
		const events = Player.events._events[evt] || [];
		for (let handler of events) {
			if (await handler(...data) === false) {
				return;
			}
		}
	},

	/**
	 * 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.
	 */
	getHandler: function (handler) {
		return typeof handler === 'string' ? _get(Player, handler) : handler;
	}
}


/***/ }),

/***/ "./src/components/footer.js":
/*!**********************************!*\
  !*** ./src/components/footer.js ***!
  \**********************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = {
	initialize: function () {
		Player.on('playsound', Player.footer.render);
		Player.on('add', Player.footer.render);
		Player.on('config', property => property === 'footerTemplate' && Player.footer.render());
		Player.on('order', () => setTimeout(Player.footer.render, 0));
	},

	render: function () {
		Player.$(`.${ns}-footer`).innerHTML = Player.templates.footer();
	}
};


/***/ }),

/***/ "./src/components/header.js":
/*!**********************************!*\
  !*** ./src/components/header.js ***!
  \**********************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = {
	options: {
		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' },
		},
		viewStyle: {
			playlist: { title: 'Hide Playlist', text: '[+]', class: 'fa-compress' },
			image: { title: 'Show Playlist', text: '[-]', class: 'fa-expand' }
		},
		hoverImages: {
			true: { title: 'Hover Images Enabled', text: '[H]', class: 'fa-picture-o' },
			false: { title: 'Hover Images Disabled', text: '[-]', class: 'fa-picture-o disabled' },
		}
	},

	delegatedEvents: {
		click: {
			[`.${ns}-shuffle-button`]: 'header.toggleShuffle',
			[`.${ns}-repeat-button`]: 'header.toggleRepeat',
			[`.${ns}-reload-button`]: e => { e.preventDefault(); Player.playlist.refresh() }
		}
	},

	initialize: function () {
		Player.on('playsound', Player.header.render);
	},

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

	/**
	 * Toggle the repeat style.
	 */
	toggleRepeat: function (e) {
		try {
			e.preventDefault();
			const options = Object.keys(Player.header.options.repeat);
			const current = options.indexOf(Player.config.repeat);
			Player.config.repeat = options[(current + 4) % 3];
			Player.header.render();
			Player.settings.save();
		} catch (err) {
			_logError('There was an error changing the repeat setting. Please check the console for details.', 'warning');
			console.error('[4chan sounds player]', err);
		}
	},

	/**
	 * Toggle the shuffle style.
	 */
	toggleShuffle: function (e) {
		try {
			e.preventDefault();
			Player.config.shuffle = !Player.config.shuffle;
			Player.header.render();

			// Update the play order.
			if (!Player.config.shuffle) {
				Player.sounds.sort((a, b) => a.id - b.id);
			} else {
				const sounds = Player.sounds;
				for (let i = sounds.length - 1; i > 0; i--) {
					const j = Math.floor(Math.random() * (i + 1));
					[sounds[i], sounds[j]] = [sounds[j], sounds[i]];
				}
			}
			Player.playlist.render();
			Player.settings.save();
			Player.trigger('order');
		} catch (err) {
			_logError('There was an error changing the shuffle setting. Please check the console for details.', 'warning');
			console.error('[4chan sounds player]', err);
		}
	}
}


/***/ }),

/***/ "./src/components/hotkeys.js":
/*!***********************************!*\
  !*** ./src/components/hotkeys.js ***!
  \***********************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

const settingsConfig = __webpack_require__(/*! ../settings */ "./src/settings.js");

module.exports = {
	initialize: function () {
		Player.on('rendered', Player.hotkeys.apply);
	},

	_keyMap: {
		' ': 'space',
		'arrowleft': 'left',
		'arrowright': 'right',
		'arrowup': 'up',
		'arrowdown': 'down'
	},

	addHandler: () => {
		Player.hotkeys.removeHandler();
		document.body.addEventListener('keydown', Player.hotkeys.handle);
	},
	removeHandler: () => {
		document.body.removeEventListener('keydown', Player.hotkeys.handle);
	},

	/**
	 * Apply the selecting hotkeys option
	 */
	apply: function () {
		const type = Player.config.hotkeys;
		Player.hotkeys.removeHandler();
		Player.off('show', Player.hotkeys.addHandler);
		Player.off('hide', Player.hotkeys.removeHandler);

		if (type === 'always') {
			// If hotkeys are always enabled then just set the handler.
			Player.hotkeys.addHandler();
		} else if (type === 'open') {
			// If hotkeys are only enabled with the player toggle the handler as the player opens/closes.
			// If the player is already open set the handler now.
			if (!Player.isHidden) {
				Player.hotkeys.addHandler();
			}
			Player.on('show', Player.hotkeys.addHandler);
			Player.on('hide', Player.hotkeys.removeHandler);
		}
	},

	/**
	 * Handle a keydown even on the body
	 */
	handle: function (e) {
		// Ignore events on inputs so you can still type.
		const ignoreFor = [ 'INPUT', 'SELECT', 'TEXTAREA', 'INPUT' ];
		if (ignoreFor.includes(e.target.nodeName) || Player.isHidden && (Player.config.hotkeys !== 'always' || !Player.sounds.length)) {
			return;
		}
		const k = e.key.toLowerCase();
		const bindings = Player.config.hotkey_bindings || {};

		// Look for a matching hotkey binding
		for (let key in bindings) {
			const keyDef = bindings[key];
			const bindingConfig = k === keyDef.key
				&& (!!keyDef.shiftKey === !!e.shiftKey) && (!!keyDef.ctrlKey === !!e.ctrlKey) && (!!keyDef.metaKey === !!e.metaKey)
				&& (!keyDef.ignoreRepeat || !e.repeat)
				&& settingsConfig.find(s => s.property === 'hotkey_bindings').settings.find(s => s.property === 'hotkey_bindings.' + key);

			if (bindingConfig) {
				e.preventDefault();
				return _get(Player, bindingConfig.keyHandler)();
			}
		}
	},

	/**
	 * Turn a hotkey definition or key event into an input string.
	 */
	stringifyKey: function (key) {
		let k = key.key.toLowerCase();
		Player.hotkeys._keyMap[k] && (k = Player.hotkeys._keyMap[k])
		return (key.ctrlKey ? 'Ctrl+' : '') + (key.shiftKey ? 'Shift+' : '') + (key.metaKey ? 'Meta+' : '') + k;
	},


	/**
	 * Turn an input string into a hotkey definition object.
	 */
	parseKey: function (str) {
		const keys = str.split('+');
		let key = keys.pop();
		Object.keys(Player.hotkeys._keyMap).find(k => Player.hotkeys._keyMap[k] === key && (key = k));
		const newValue = { key };
		keys.forEach(key => newValue[key.toLowerCase() + 'Key'] = true);
		return newValue;
	},

	volumeUp: function () {
		Player.audio.volume = Math.min(Player.audio.volume + .05, 1);
	},

	volumeDown: function () {
		Player.audio.volume = Math.max(Player.audio.volume - .05, 0);
	}
};


/***/ }),

/***/ "./src/components/playlist.js":
/*!************************************!*\
  !*** ./src/components/playlist.js ***!
  \************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

var { parseFiles } = __webpack_require__(/*! ../file_parser */ "./src/file_parser.js");

module.exports = {
	atRoot: [ 'add', 'remove' ],

	delegatedEvents: {
		click: {
			[`.${ns}-viewStyle-button`]: 'playlist.toggleView',
			[`.${ns}-hoverImages-button`]: 'playlist.toggleHoverImages',
			[`.${ns}-remove-link`]: 'playlist.handleRemove',
			[`.${ns}-list-item`]: 'playlist.handleSelect'
		},
		mousemove: { [`.${ns}-list-item`]: 'playlist.moveHoverImage' },
		dragstart: { [`.${ns}-list-item`]: 'playlist.handleDragStart' },
		dragenter: { [`.${ns}-list-item`]: 'playlist.handleDragEnter' },
		dragend: { [`.${ns}-list-item`]: 'playlist.handleDragEnd' },
		dragover: { [`.${ns}-list-item`]: e => e.preventDefault() },
		drop: { [`.${ns}-list-item`]: e => e.preventDefault() }
	},

	undelegatedEvents: {
		click: {
			body: 'playlist.closeMenus'
		},
		keydown: {
			body: e => e.key === 'Escape' && Player.playlist.closeMenus()
		},
		mouseenter: {
			[`.${ns}-list-item`]: 'playlist.showHoverImage'
		},
		mouseleave: {
			[`.${ns}-list-item`]: 'playlist.removeHoverImage'
		}
	},

	initialize: function () {
		Player.on('playsound', sound => {
			Player.playlist.showImage(sound);
			Player.playlist.render();
		});
	},

	/**
	 * Render the playlist.
	 */
	render: function () {
		if (!Player.container) {
			return;
		}
		if (Player.$(`.${ns}-list-container`)) {
			Player.$(`.${ns}-list-container`).innerHTML = Player.templates.list();
		}
		Player.events.addUndelegatedListeners({
			mouseenter: Player.playlist.undelegatedEvents.mouseenter,
			mouseleave:  Player.playlist.undelegatedEvents.mouseleave
		});
	},

	/**
	 * Update the image displayed in the player.
	 */
	showImage: function (sound, thumb) {
		if (!Player.container) {
			return;
		}
		let isVideo = Player.playlist.isVideo = !thumb && sound.image.endsWith('.webm');
		try {
			Player.$(`.${ns}-image`).src = isVideo || thumb ? sound.thumb : sound.image;
			Player.$(`.${ns}-image-link`).href = sound.image;
			if (isVideo) {
				Player.$(`.${ns}-video`).src = sound.image;
				Player.$(`.${ns}-image-link`).classList.add(ns + '-show-video');
			} else {
				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);
		}
	},

	/**
	 * Switch between playlist and image view.
	 */
	toggleView: function (e) {
		if (!Player.container) {
			return;
		}
		e && e.preventDefault();
		let style = Player.config.viewStyle === 'playlist' ? 'image' : 'playlist';
		try {
			Player.display.setViewStyle(style);
			Player.header.render();
			Player.settings.save();
		} 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 };

			// Add the sound with the location based on the shuffle settings.
			let index = Player.config.shuffle
				? Math.floor(Math.random() * Player.sounds.length - 1)
				: Player.sounds.findIndex(s => s.id > id);
			index < 0 && (index = Player.sounds.length);
			Player.sounds.splice(index, 0, sound);

			if (Player.container) {
				// Re-render the list.
				Player.playlist.render();

				// If nothing else has been added yet show the image for this sound.
				if (Player.sounds.length === 1) {
					// If we're on a thread with autoshow enabled then make sure the player is displayed
					if (/\/thread\//.test(location.href) && Player.config.autoshow) {
						Player.show();
					}
					Player.playlist.showImage(sound);
				}
				Player.trigger('add', 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);
		}
	},

	/**
	 * Remove a sound
	 */
	remove: function (sound) {
		const index = Player.sounds.indexOf(sound);

		// If the playing sound is being removed then play the next sound.
		if (Player.playing === sound) {
			Player.pause();
			Player.next(true);
		}
		// Remove the sound from the the list and play order.
		index > -1 && Player.sounds.splice(index, 1);

		// Re-render the list.
		Player.playlist.render();
		Player.$(`.${ns}-count`).innerHTML = Player.sounds.length;
	},

	/**
	 * Handle a click on the remove link
	 */
	handleRemove: function (e) {
		const id = e.eventTarget.closest(`.${ns}-list-item`).getAttribute('data-id');
		const sound = id && Player.sounds.find(sound => sound.id === '' + id);
		sound && Player.remove(sound);
	},

	/**
	 * Close any open menus, except for one belonging to an item that was clicked.
	 */
	closeMenus: function (e) {
		const clickedListItem = e && e.target.closest(`.${ns}-list-item`);

		document.querySelectorAll(`.${ns}-item-menu`).forEach(menu => {
			const row = menu.parentNode;
			// Ignore for a list item that was clicked. The handleSelect below will deal with it.
			if (row === clickedListItem) {
				return;
			}
			row.removeChild(menu);
			row.classList.remove(`.${ns}-has-menu`);
		});
	},

	/**
	 * Handle an playlist item being clicked. Either open/close the menu or play the sound.
	 */
	handleSelect: function (e) {
		const clickedMenu = e.target.closest(`.${ns}-item-menu`);
		const menu = clickedMenu || e.eventTarget.querySelector(`.${ns}-item-menu`);

		const id = e.eventTarget.getAttribute('data-id');
		const clickedMenuButton = e.target.closest(`.${ns}-item-menu-button`);
		const sound = id && Player.sounds.find(sound => sound.id === '' + id);

		// Remove the menu.
		if (menu) {
			e.eventTarget.removeChild(menu);
			e.eventTarget.classList.remove(`.${ns}-has-menu`);

		// If the manu wasn't showing and menu button was clicked go ahead and show the menu.
		} else if (clickedMenuButton) {
			e.preventDefault();
			if (e.eventTarget.hoverImage) {
				e.eventTarget.hoverImage.parentNode.removeChild(e.eventTarget.hoverImage);
				delete e.eventTarget.hoverImage;
			}
			// Create the menu.
			const container = document.createElement('div');
			container.innerHTML = Player.templates.itemMenu({
				top: e.clientY,
				left: e.clientX,
				sound
			});
			const dialog = container.children[0];

			// Update the row with it.
			e.eventTarget.appendChild(dialog);
			e.eventTarget.classList.remove(`.${ns}-has-menu`);

			// Make sure it's within the page.
			const style = document.defaultView.getComputedStyle(dialog);
			const width = parseInt(style.width, 10);
			const height = parseInt(style.height, 10);
			// Show the dialog to the left of the cursor, if there's room.
			if (e.clientX - width > 0) {
				dialog.style.left = e.clientX - width + 'px';
			}
			// Move the dialog above the cursor if it's off screen.
			if (e.clientY + height > document.documentElement.clientHeight - 40) {
				dialog.style.top = e.clientY - height + 'px';
			}
			// Add the focused class handler
			dialog.querySelectorAll('.entry').forEach(el => {
				el.addEventListener('mouseenter', Player.playlist.setFocusedMenuItem);
				el.addEventListener('mouseleave', Player.playlist.unsetFocusedMenuItem);
			});
		}

		// If the menu or menu button was clicked don't play the sound.
		if (clickedMenuButton || clickedMenu) {
			return;
		}

		e.preventDefault();
		sound && Player.play(sound);
	},

	setFocusedMenuItem: function (e) {
		e.currentTarget.classList.add('focused');
	},

	unsetFocusedMenuItem: function (e) {
		e.currentTarget.classList.remove('focused');
	},

	refresh: function () {
		parseFiles(document.body);
	},

	toggleHoverImages: function (e) {
		e && e.preventDefault();
		Player.config.hoverImages = !Player.config.hoverImages;
		Player.header.render();
		Player.settings.save();
	},

	showHoverImage: function (e) {
		// Make sure there isn't already an image, hover images are enabled, and there isn't an open menu.
		if (e.currentTarget.hoverImage || !Player.config.hoverImages || Player.$(`.${ns}-item-menu`)) {
			return;
		}
		const id = e.currentTarget.getAttribute('data-id');
		const sound = Player.sounds.find(sound => sound.id === '' + id);
		const hoverImage = document.createElement('img');

		// Add it to the list so the mouseleave triggers properly
		e.currentTarget.parentNode.appendChild(hoverImage);
		e.currentTarget.hoverImage = hoverImage;
		hoverImage.setAttribute('class', `${ns}-hover-image`);
		hoverImage.setAttribute('src', sound.thumb);
		Player.playlist.positionHoverImage(e, hoverImage);
	},

	moveHoverImage: function (e) {
		if (e.eventTarget.hoverImage) {
			Player.playlist.positionHoverImage(e, e.eventTarget.hoverImage);
		}
	},

	positionHoverImage: function(e, image) {
		const { width, height } = image.getBoundingClientRect();
		const maxX = document.documentElement.clientWidth - width - 5;
		image.style.left = (Math.min(e.clientX, maxX) + 5) + 'px';
		image.style.top = (e.clientY - height - 10) + 'px';
	},

	removeHoverImage: function (e) {
		e.currentTarget.hoverImage && (e.currentTarget.parentNode.removeChild(e.currentTarget.hoverImage));
		delete e.currentTarget.hoverImage;
	},

	handleDragStart: function (e) {
		Player.playlist._dragging = e.eventTarget;
		Player._hoverImages = Player.config.hoverImages;
		Player.config.hoverImages = false;
		e.eventTarget.classList.add(`${ns}-dragging`);
		e.dataTransfer.setDragImage(new Image(), 0, 0);
		e.dataTransfer.dropEffect = 'move';
		e.dataTransfer.setData('text/plain', e.eventTarget.getAttribute('data-id'));
	},

	handleDragEnter: function (e) {
		e.preventDefault();
		const moving = Player.playlist._dragging;
		const id = moving.getAttribute('data-id');
		let before = e.target.closest && e.target.closest(`.${ns}-list-item`);
		if (!before || moving === before) {
			return;
		}
		const movingIdx = Player.sounds.findIndex(s => s.id === id);
		const list = moving.parentNode;

		// If the item is being moved down it need inserting before the node after the one it's dropped on.
		const position = moving.compareDocumentPosition(before);
		if (position & 0x04) {
			before = before.nextSibling;
		}

		// Move the element and sound.
		// If there's nothing to go before then append.
		if (before) {
			const beforeId = before.getAttribute('data-id');
			const beforeIdx = Player.sounds.findIndex(s => s.id === beforeId);
			const insertIdx = movingIdx < beforeIdx ? beforeIdx - 1 : beforeIdx;
			list.insertBefore(moving, before);
			Player.sounds.splice(insertIdx, 0, Player.sounds.splice(movingIdx, 1)[0]);
		} else {
			Player.sounds.push(Player.sounds.splice(movingIdx, 1)[0]);
			list.appendChild(moving);
		}
		Player.trigger('order');
	},

	handleDragEnd: function (e) {
		e.preventDefault();
		delete Player.playlist._dragging;
		e.eventTarget.classList.remove(`${ns}-dragging`);
		Player.config.hoverImages = Player._hoverImages;
		Player.playlist.render();
	}
};


/***/ }),

/***/ "./src/components/position.js":
/*!************************************!*\
  !*** ./src/components/position.js ***!
  \************************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = {
	delegatedEvents: {
		mousedown: {
			[`.${ns}-title`]: 'position.initMove',
			[`.${ns}-expander`]: 'position.initResize'
		}
	},

	initialize: function () {
		// Apply the last position/size when the player is shown.
		Player.on('show', async function () {
			const [ top, left ] = (await GM.getValue(ns + '.position') || '').split(':');
			const [ width, height ] = (await GM.getValue(ns + '.size') || '').split(':');
			+top && +left && Player.position.move(top, left, true);
			+width && +height && Player.position.resize(width, height);
		});
	},

	/**
	 * Handle the user grabbing the expander.
	 */
	initResize: function initDrag(e) {
		e.preventDefault();
		Player._startX = e.clientX;
		Player._startY = e.clientY;
		let { width, height } = Player.container.getBoundingClientRect();
		Player._startWidth = width;
		Player._startHeight = height;
		document.documentElement.addEventListener('mousemove', Player.position.doResize, false);
		document.documentElement.addEventListener('mouseup', Player.position.stopResize, false);
	},

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

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

	/**
	 * Resize the player.
	 */
	resize: function (width, height) {
		if (!Player.container) {
			return;
		}
		const { bottom } = Player.position.getHeaderOffset();
		// Make sure the player isn't going off screen.
		height = Math.min(height, document.documentElement.clientHeight - Player.container.offsetTop - bottom);
		width = Math.min(width - 2, document.documentElement.clientWidth - Player.container.offsetLeft);

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

		// Change the height of the playlist or image.
		const heightElement = Player.config.viewStyle === 'playlist' ? Player.$(`.${ns}-list-container`)
			: Player.config.viewStyle === 'image' ? Player.$(`.${ns}-image-link`)
			: Player.config.viewStyle === 'settings' ? Player.$(`.${ns}-settings`) : null;

		const offset = Player.container.getBoundingClientRect().height - heightElement.getBoundingClientRect().height;
		heightElement.style.height = Math.max(10, height - offset) + 'px';
	},

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

		// Try to reapply the current sizing to fix oversized winows.
		const { width, height } = Player.container.getBoundingClientRect();
		Player.position.resize(width, height);

		Player._offsetX = e.clientX - Player.container.offsetLeft;
		Player._offsetY = e.clientY - Player.container.offsetTop;
		document.documentElement.addEventListener('mousemove', Player.position.doMove, false);
		document.documentElement.addEventListener('mouseup', Player.position.stopMove, false);
	},

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

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

	/**
	 * Move the player.
	 */
	move: function (x, y, allowOffscreen) {
		if (!Player.container) {
			return;
		}

		const { top, bottom } = Player.position.getHeaderOffset();

		// Ensure the player stays fully within the window.
		const { width, height } = Player.container.getBoundingClientRect();
		const maxX = allowOffscreen ? Infinity : document.documentElement.clientWidth - width;
		const maxY = allowOffscreen ? Infinity : document.documentElement.clientHeight - height - bottom;

		// Move the window.
		Player.container.style.left = Math.max(0, Math.min(x, maxX)) + 'px';
		Player.container.style.top = Math.max(top, Math.min(y, maxY)) + 'px';
	},

	/**
	 * Get the offset from the top or bottom required for the 4chan X header.
	 */
	getHeaderOffset: function () {
		const docClasses = document.documentElement.classList;
		const hasChanXHeader = docClasses.contains('fixed');
		const headerHeight = hasChanXHeader ? document.querySelector('#header-bar').getBoundingClientRect().height : 0;
		const top = hasChanXHeader && docClasses.contains('top-header') ? headerHeight : 0;
		const bottom = hasChanXHeader && docClasses.contains('bottom-header') ? headerHeight : 0;

		return { top, bottom };
	}
}


/***/ }),

/***/ "./src/components/settings.js":
/*!************************************!*\
  !*** ./src/components/settings.js ***!
  \************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

const settingsConfig = __webpack_require__(/*! settings */ "./src/settings.js");

module.exports = {
	delegatedEvents: {
		click: {
			[`.${ns}-config-button`]: 'settings.toggle',
			[`.${ns}-setting-action`]: 'settings.handleAction',
		},
		focusout: {
			[`.${ns}-settings input, .${ns}-settings textarea`]: 'settings.handleChange'
		},
		change: {
			[`.${ns}-settings input[type=checkbox], .${ns}-settings select`]: 'settings.handleChange'
		},
		keydown: {
			[`.${ns}-key-input`]: 'settings.handleKeyChange'
		}
	},

	initialize: async function () {
		// Apply the default board theme as default.
		Player.settings.applyBoardTheme();

		// Apply the default config.
		Player.config = settingsConfig.reduce(function reduceSettings(config, settingConfig) {
			if (settingConfig.settings) {
				return settingConfig.settings.reduce(reduceSettings, config);
			}
			return _set(config, settingConfig.property, settingConfig.default);
		}, {});

		// Load the user config.
		await Player.settings.load();

		// Listen for the player closing to apply the pause on hide setting.
		Player.on('hide', function () {
			if (Player.config.pauseOnHide) {
				Player.pause();
			}
		});
	},

	render: function () {
		Player.$(`.${ns}-settings`).innerHTML = Player.templates.settings();
	},

	forceBoardTheme: function () {
		Player.settings.applyBoardTheme(true);
		Player.settings.save();
	},

	applyBoardTheme: function (force) {
		// Create a reply element to gather the style from
		const div = document.createElement('div');
		div.setAttribute('class', is4chan ? 'post reply' : 'post_wrapper');
		document.body.appendChild(div);
		const style = document.defaultView.getComputedStyle(div);

		// Apply the computed style to the color config.
		const colorSettingMap = {
			'colors.text': 'color',
			'colors.background': 'backgroundColor',
			'colors.odd_row': 'backgroundColor',
			'colors.border': 'borderBottomColor',
			// If the border is the same color as the text don't use it as a background color.
			'colors.even_row': style.borderBottomColor === style.color ? 'backgroundColor' : 'borderBottomColor'
		}
		settingsConfig.find(s => s.property === 'colors').settings.forEach(setting => {
			const updateConfig = force || (setting.default === _get(Player.config, setting.property));
			colorSettingMap[setting.property] && (setting.default = style[colorSettingMap[setting.property]]);
			updateConfig && _set(Player.config, setting.property, setting.default);
		});

		// Clean up the element.
		document.body.removeChild(div);
		delete div;

		// Updated the stylesheet if it exists.
		Player.stylesheet && Player.display.updateStylesheet();

		// Re-render the settings if needed.
		Player.container && Player.settings.render();
	},

	/**
	 * Persist the player settings.
	 */
	save: function () {
		try {
			// Filter settings that have been modified from the default.
			const settings = settingsConfig.reduce(function _handleSetting(settings, setting) {
				if (setting.settings) {
					return setting.settings.reduce(_handleSetting, settings);
				}
				const userVal = _get(Player.config, setting.property);
				if (userVal !== undefined && userVal !== setting.default) {
					_set(settings, setting.property, userVal);
				}
				return settings;
			}, {});
			// Save the settings.
			return GM.setValue(ns + '.settings', JSON.stringify(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.
	 */
	load: async function () {
		try {
			let settings = await GM.getValue(ns + '.settings');
			if (!settings) {
				return;
			}
			try {
				settings = JSON.parse(settings);
			} catch(e) {
				console.error(e);
				return;
			}
			_mix(Player.config, 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);
		}
	},

	/**
	 * Toggle whether the player or settings are displayed.
	 */
	toggle: function (e) {
		try {
			e.preventDefault();
			if (Player.config.viewStyle === 'settings') {
				Player.display.setViewStyle(Player._preSettingsView || 'playlist');
			} else {
				Player._preSettingsView = Player.config.viewStyle;
				Player.display.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.
	 */
	handleChange: function (e) {
		try {
			const input = e.eventTarget;
			const property = input.getAttribute('data-property');
			let settingConfig;
			settingsConfig.find(function searchConfig(setting) {
				if (setting.property === property) {
					return settingConfig = setting;
				}
				if (setting.settings) {
					let subSetting = setting.settings.find(_setting => _setting.property === property);
					return subSetting && (settingConfig = { ...setting, settings: null, ...subSetting });
				}
				return false;
			});

			// Get the new value of the setting.
			const currentValue = _get(Player.config, property);
			let newValue = input[input.getAttribute('type') === 'checkbox' ? 'checked' : 'value'];

			if (settingConfig.parse) {
				newValue = _get(Player, settingConfig.parse)(newValue);
			}
			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.config, property, newValue);

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

				// Save the new settings.
				Player.settings.save();

				Player.trigger('config', property, newValue, currentValue);
			}

			// Run any handler required by the value changing
			settingConfig && settingConfig.handler && _get(Player, settingConfig.handler, () => null)(newValue);
		} catch (err) {
			_logError('There was an error updating the setting. Please check the console for details.');
			console.error('[4chan sounds player]', err);
		}
	},

	handleKeyChange: function (e) {
		e.preventDefault();
		if (e.key === 'Shift' || e.key === 'Control' || e.key === 'Meta') {
			return;
		}
		e.eventTarget.value = Player.hotkeys.stringifyKey(e);
	},

	handleAction: function (e) {
		e.preventDefault();
		const handlerName = e.eventTarget.getAttribute('data-handler');
		const handler = _get(Player, handlerName);
		handler && handler();
	}
}


/***/ }),

/***/ "./src/file_parser.js":
/*!****************************!*\
  !*** ./src/file_parser.js ***!
  \****************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = {
	parseFiles,
	parsePost
}

function parseFiles (target) {
	target.querySelectorAll('.post').forEach(parsePost);
};

function parsePost(post) {
	try {
		const parentParent = post.parentElement.parentElement;
		if (parentParent.id === 'qp' || parentParent.classList.contains('inline') || post.parentElement.classList.contains('noFile')) {
			return;
		}

		let fileName = null;

		if (!is4chan) {
			const fileLink = post.querySelector('.post_file_filename');
			fileName = fileLink && fileLink.title;
		} else if (isChanX) {
			[
				post.querySelector('.fileText .file-info .fnfull'),
				post.querySelector('.fileText .file-info > a')
			].some(function (node) {
				return node && (fileName = node.textContent);
			});
		} else {
			[
				post.querySelector('.fileText'),
				post.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(is4chan ? 1 : 0);
		const name = match[1] || id;
		const fileThumb = post.querySelector(is4chan ? '.fileThumb' : '.thread_image_link');
		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.config.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);
	}
};


/***/ }),

/***/ "./src/globals.js":
/*!************************!*\
  !*** ./src/globals.js ***!
  \************************/
/*! no static exports found */
/***/ (function(module, exports) {

/**
 * Global variables and helpers.
 */

window.ns = 'fc-sounds';

window.is4chan = location.hostname.includes('4chan.org') || location.hostname.includes('4channel.org');
window.isChanX = document.documentElement.classList.contains('fourchan-x');

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

window._set = function(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;
};

window._get = function(object, path, dflt) {
	const props = path.split('.');
	return props.reduce((obj, k) => obj && obj[k], object) || dflt;
};

window.toDuration = function(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;
};

window._mix = 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];
		}
	}
};


/***/ }),

/***/ "./src/main.js":
/*!*********************!*\
  !*** ./src/main.js ***!
  \*********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _globals__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./globals */ "./src/globals.js");
/* harmony import */ var _globals__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_globals__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _player__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./player */ "./src/player.js");
/* harmony import */ var _player__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_player__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _file_parser__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./file_parser */ "./src/file_parser.js");
/* harmony import */ var _file_parser__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_file_parser__WEBPACK_IMPORTED_MODULE_2__);






async function doInit () {
	// The player tends to be all black without this timeout.
	// Something with the timing of the stylesheet loading and applying the board theme.
	setTimeout(async function () {
		await _player__WEBPACK_IMPORTED_MODULE_1___default.a.initialize();

		Object(_file_parser__WEBPACK_IMPORTED_MODULE_2__["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) {
							Object(_file_parser__WEBPACK_IMPORTED_MODULE_2__["parseFiles"])(node);
						}
					});
				}
			});
		});

		observer.observe(document.body, {
			childList: true,
			subtree: true
		});
	}, 0);
}

document.addEventListener('4chanXInitFinished', function () {
	if (isChanX) {
		doInit();
	}
	isChanX = true;
	_player__WEBPACK_IMPORTED_MODULE_1___default.a.display.initChanX();
});

if (!isChanX) {
	document.addEventListener('DOMContentLoaded', doInit);
}



/***/ }),

/***/ "./src/player.js":
/*!***********************!*\
  !*** ./src/player.js ***!
  \***********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

const components = {
	controls: __webpack_require__(/*! ./components/controls */ "./src/components/controls.js"),
	display: __webpack_require__(/*! ./components/display */ "./src/components/display.js"),
	events: __webpack_require__(/*! ./components/events */ "./src/components/events.js"),
	footer: __webpack_require__(/*! ./components/footer */ "./src/components/footer.js"),
	header: __webpack_require__(/*! ./components/header */ "./src/components/header.js"),
	hotkeys: __webpack_require__(/*! ./components/hotkeys */ "./src/components/hotkeys.js"),
	playlist: __webpack_require__(/*! ./components/playlist */ "./src/components/playlist.js"),
	position: __webpack_require__(/*! ./components/position */ "./src/components/position.js"),
	settings: __webpack_require__(/*! ./components/settings */ "./src/components/settings.js")
};

// Create a global ref to the player.
const Player = window.Player = module.exports = {
	ns,

	audio: new Audio(),
	sounds: [],
	isHidden: true,
	currentIndex: 0,
	container: null,
	ui: {},
	_progressBarStyleSheets: {},

	// Build the config from the default
	config: {},

	// Helper function to query elements in the player.
	$: (...args) => Player.container && Player.container.querySelector(...args),

	// Store a ref to the components so they can be iterated.
	components,

	// Get all the templates.
	templates: {
		// Settings must be first.
		settings: __webpack_require__(/*! ./templates/settings.tpl */ "./src/templates/settings.tpl"),
		css: __webpack_require__(/*! ./scss/style.scss */ "./src/scss/style.scss"),
		body: __webpack_require__(/*! ./templates/body.tpl */ "./src/templates/body.tpl"),
		header: __webpack_require__(/*! ./templates/header.tpl */ "./src/templates/header.tpl"),
		player: __webpack_require__(/*! ./templates/player.tpl */ "./src/templates/player.tpl"),
		controls: __webpack_require__(/*! ./templates/controls.tpl */ "./src/templates/controls.tpl"),
		list: __webpack_require__(/*! ./templates/list.tpl */ "./src/templates/list.tpl"),
		itemMenu: __webpack_require__(/*! ./templates/item_menu.tpl */ "./src/templates/item_menu.tpl"),
		footer: __webpack_require__(/*! ./templates/footer.tpl */ "./src/templates/footer.tpl")
	},

	/**
	 * Set up the player.
	 */
	initialize: async function initialize() {
		if (Player.initialized) {
			return;
		}
		Player.initialized = true;
		try {
			Player.sounds = [ ];
			// Run the initialisation for each component.
			for (let name in components) {
				components[name].initialize && await components[name].initialize();
			}

			if (!is4chan) {
				// Add a sounds link in the nav for archives
				const nav = document.querySelector('.navbar-inner .nav:nth-child(2)');
				const li = document.createElement('li');
				const showLink = document.createElement('a');
				showLink.innerHTML = 'Sounds';
				showLink.href = 'javascript;'
				li.appendChild(showLink);
				nav.appendChild(li);
				showLink.addEventListener('click', Player.display.toggle);
			} else if (isChanX) {
				// If it's already known that 4chan X is running then setup the button for it.
				Player.display.initChanX()
			} else {
				// Add the [Sounds] link in the top and bottom nav.
				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.display.toggle);
				});
			}

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

// Add each of the components to the player.
for (let name in components) {
	Player[name] = components[name];
	(Player[name].atRoot || []).forEach(k => Player[k] = Player[name][k]);
}


/***/ }),

/***/ "./src/scss/style.scss":
/*!*****************************!*\
  !*** ./src/scss/style.scss ***!
  \*****************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = data => `audio {
  width: 100%;
}

.${ns}-controls {
  align-items: center;
  padding: 0.5rem;
  border-bottom: solid 1px ${Player.config.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: 0.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: 0.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: flex-end;
  align-items: center;
}
.${ns}-progress-bar .${ns}-full-bar .${ns}-current-bar:after {
  content: "";
  background: white;
  height: 0.8rem;
  min-width: 0.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}-footer {
  padding: 0.15rem 0.25rem;
  border-top: solid 1px ${Player.config.colors.border};
}
.${ns}-footer .${ns}-expander {
  position: absolute;
  bottom: 0px;
  right: 0px;
  height: 0.75rem;
  width: 0.75rem;
  cursor: se-resize;
  background: linear-gradient(to bottom right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 50%, ${Player.config.colors.border} 55%, ${Player.config.colors.border} 100%);
}

.${ns}-title {
  cursor: grab;
  text-align: center;
  border-bottom: solid 1px ${Player.config.colors.border};
  padding: 0.25rem 0;
}

html.fourchan-x .${ns}-title .fa-repeat.fa-repeat-one::after {
  content: "1";
  font-size: 0.5rem;
  visibility: visible;
  margin-left: -1px;
}

.${ns}-image-link {
  text-align: center;
  display: flex;
  justify-items: center;
  justify-content: center;
  border-bottom: solid 1px ${Player.config.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: ${Player.config.colors.background};
  border: 1px solid ${Player.config.colors.border};
  min-height: 200px;
  min-width: 100px;
  color: ${Player.config.colors.text};
}

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

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

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

html.fourchan-x #${ns}-container .fa {
  font-size: 0;
  visibility: hidden;
  margin: 0 0.15rem;
}

.${ns}-truncate-text {
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
}

.${ns}-list-container {
  overflow-y: auto;
}
.${ns}-list-container .${ns}-hover-image {
  position: fixed;
  max-height: 125px;
  max-width: 125px;
}
.${ns}-list-container .${ns}-list-item {
  list-style-type: none;
  padding: 0.15rem 0.25rem;
  white-space: nowrap;
  text-overflow: ellipsis;
  cursor: pointer;
  background: ${Player.config.colors.odd_row};
  overflow: hidden;
  height: 1rem;
}
.${ns}-list-container .${ns}-list-item.playing {
  background: ${Player.config.colors.playing} !important;
}
.${ns}-list-container .${ns}-list-item:nth-child(2n) {
  background: ${Player.config.colors.even_row};
}
.${ns}-list-container .${ns}-list-item .${ns}-item-menu-button {
  right: 0.25rem;
  display: none;
}
.${ns}-list-container .${ns}-list-item:hover .${ns}-item-menu-button {
  display: inline-block;
}
.${ns}-list-container .${ns}-list-item .${ns}-item-menu {
  position: fixed;
  background: ${Player.config.colors.background};
  background: ${Player.config.colors.background};
  border-color: ${Player.config.colors.border};
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
  border-radius: 3px;
  padding-top: 1px;
  padding-bottom: 3px;
}
.${ns}-list-container .${ns}-list-item .${ns}-item-menu a.entry {
  margin: 0.25rem;
  display: block;
}
.${ns}-list-container .${ns}-list-item.${ns}-dragging {
  background: ${Player.config.colors.dragging};
}

.${ns}-settings {
  display: none;
  padding: 0 0.25rem;
  height: 100%;
  overflow: auto;
}
.${ns}-settings .${ns}-setting-header {
  font-weight: 600;
  margin: 0.5rem 0;
}
.${ns}-settings .${ns}-setting-action {
  font-weight: normal;
  text-decoration: underline;
  margin-left: 0.25rem;
}
.${ns}-settings textarea {
  border: solid 1px ${Player.config.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;
}`

/***/ }),

/***/ "./src/settings.js":
/*!*************************!*\
  !*** ./src/settings.js ***!
  \*************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = [
	{
		property: 'shuffle',
		default: false
	},
	{
		property: 'repeat',
		default: 'all'
	},
	{
		property: 'viewStyle',
		default: 'playlist'
	},
	{
		property: 'hoverImages',
		default: false
	},
	{
		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: 'hotkeys',
		default: 'open',
		title: 'Hotkeys',
		description: 'Enable hot keys for controlling the player playback.',
		showInSettings: true,
		handler: 'hotkeys.apply',
		options: [
			[ 'always', 'Always' ],
			[ 'open', 'Only with the player open' ],
			[ 'never', 'Never' ]
		]
	},
	{
		title: 'Hotkey Bindings',
		showInSettings: true,
		format: 'hotkeys.stringifyKey',
		parse: 'hotkeys.parseKey',
		class: `${ns}-key-input`,
		property: 'hotkey_bindings',
		settings: [
			{
				property: 'hotkey_bindings.playPause',
				title: 'Play/Pause',
				keyHandler: 'togglePlay',
				ignoreRepeat: true,
				default: { key: ' ' }
			},
			{
				property: 'hotkey_bindings.previous',
				title: 'Previous',
				keyHandler: 'previous',
				ignoreRepeat: true,
				default: { key: 'arrowleft' }
			},
			{
				property: 'hotkey_bindings.next',
				title: 'Next',
				keyHandler: 'next',
				ignoreRepeat: true,
				default: { key: 'arrowright' }
			},
			{
				property: 'hotkey_bindings.volumeUp',
				title: 'Volume Up',
				keyHandler: 'hotkeys.volumeUp',
				default: { shiftKey: true, key: 'arrowup' }
			},
			{
				property: 'hotkey_bindings.volumeDown',
				title: 'Volume Down',
				keyHandler: 'hotkeys.volumeDown',
				default: { shiftKey: true, key: 'arrowdown' }
			},
			{
				property: 'hotkey_bindings.togglePlayer',
				title: 'Show/Hide',
				keyHandler: 'display.toggle',
				default: { key: 'h' }
			},
			{
				property: 'hotkey_bindings.togglePlaylist',
				title: 'Toggle Playlist',
				keyHandler: 'playlist.toggleView',
				default: { key: '' }
			},
			{
				property: 'hotkey_bindings.toggleHoverImages',
				title: 'Toggle Hover Images',
				keyHandler: 'playlist.toggleHoverImages',
				default: { key: '' }
			}
		]
	},
	{
		property: 'footerTemplate',
		title: 'Footer Contents',
		default: '%p / %t sounds\npostlink:"Post"\nimagelink:"Image"\nsoundlink:"Sound"',
		description: 'What the footer displays. %p is the playing index. %t is the total sound count. postlink, imagelink, soundlink are links.',
		showInSettings: 'textarea'
	},
	{
		property: 'allow',
		default: [
			'4cdn.org',
			'catbox.moe',
			'dmca.gripe',
			'lewd.se',
			'pomf.cat',
			'zz.ht'
		],
		title: 'Allowed Hosts',
		description: 'Which domains sources are allowed to be loaded from.',
		showInSettings: true,
		split: '\n'
	},
	{
		title: 'Colors',
		showInSettings: true,
		property: 'colors',
		actions: [
			{ title: 'Match Theme', handler: 'settings.forceBoardTheme' }
		],
		// These colors will be overriden with the theme defaults at initialization.
		settings: [
			{
				property: 'colors.text',
				default: '#000000',
				title: 'Text Color'
			},
			{
				property: 'colors.background',
				default: '#d6daf0',
				title: 'Background Color'
			},
			{
				property: 'colors.border',
				default: '#b7c5d9',
				title: 'Border Color'
			},
			{
				property: 'colors.odd_row',
				default: '#d6daf0',
				title: 'Odd Row Color',
			},
			{
				property: 'colors.even_row',
				default: '#b7c5d9',
				title: 'Even Row Color'
			},
			{
				property: 'colors.playing',
				default: '#98bff7',
				title: 'Playing Row Color'
			},
			{
				property: 'colors.dragging',
				default: '#c396c8',
				title: 'Dragging Row Color'
			},
			{
				property: 'colors.expander',
				default: '#808bbf',
				title: 'Expander Color'
			}
		]
	}
];


/***/ }),

/***/ "./src/templates/body.tpl":
/*!********************************!*\
  !*** ./src/templates/body.tpl ***!
  \********************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = data => `<div id="${ns}-container" data-view-style="${Player.config.viewStyle}" style="top: 30px; left: 0px; width: 350px; display: none;">
	<div class="${ns}-title ${ns}-row">
		${Player.templates.header(data)}
	</div>
	<div class="${ns}-view-container">
		<div class="${ns}-player">
			${Player.templates.player(data)}
		</div>
		<div class="${ns}-settings" style="height: 400px">
			${Player.templates.settings(data)}
		</div>
	</div>
	<div class="${ns}-footer">
		${Player.templates.footer(data)}
	</div>
</div>`

/***/ }),

/***/ "./src/templates/controls.tpl":
/*!************************************!*\
  !*** ./src/templates/controls.tpl ***!
  \************************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = 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>`

/***/ }),

/***/ "./src/templates/footer.tpl":
/*!**********************************!*\
  !*** ./src/templates/footer.tpl ***!
  \**********************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = data => Player.config.footerTemplate
	.replace(/%p/g, Player.currentIndex || 0)
	.replace(/%t/g, Player.sounds.length)
	.replace(/postlink(:"([^"]+)")?/g, function (full, _, text) {
		return Player.playing
			? `<a href="#${(is4chan ? 'p' : '') + Player.playing.id}">${text || 'Post'}</a>`
			: '';
	})
	.replace(/imagelink(:"([^"]+)")?/g, function (full, _, text) {
		return Player.playing
			? `<a href="${Player.playing.image}" target="_blank">${text || 'Image'}</a>`
			: '';
	})
	.replace(/soundlink(:"([^"]+)")?/g, function (full, _, text) {
		return Player.playing
			? `<a href="${Player.playing.src}" target="_blank">${text || 'Sound'}</a>`
			: '';
	})
+ `<div class="${ns}-expander"></div>`

/***/ }),

/***/ "./src/templates/header.tpl":
/*!**********************************!*\
  !*** ./src/templates/header.tpl ***!
  \**********************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = data => `<div class="${ns}-col-auto" style="margin-left: 0.25rem;">`
	+ Object.keys(Player.header.options).map(key => {
		let option = Player.header.options[key][Player.config[key]] || Player.header.options[key][Object.keys(Player.header.options[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 ${ns}-truncate-text">
	${Player.playing ? Player.playing.title : '4chan Sounds'}
</div>
<div class="${ns}-col-auto" style="margin-right: 0.25rem;">
	<a class="${ns}-reload-button fa fa-refresh" title="Reload the playlist" href="javascript;">[R]</a>
	<a class="${ns}-config-button fa fa-wrench" title="Settings" href="javascript;">[S]</a>
	<a class="${ns}-close-button fa fa-times" href="javascript;">X</a>
</div>`

/***/ }),

/***/ "./src/templates/item_menu.tpl":
/*!*************************************!*\
  !*** ./src/templates/item_menu.tpl ***!
  \*************************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = data => `<div class="${ns}-item-menu dialog" id="menu" tabindex="0" data-type="post" style="top: ${data.top}px; left: ${data.left}px;">
	<a class="${ns}-remove-link entry focused" href="javascript:;">Remove</a>
	<a class="${ns}-show-post-link entry" href="#${(is4chan ? 'p' : '') + data.sound.id}">Show Post</a>
	<a class="${ns}-show-post-link entry" href="${data.sound.image}" target="_blank">Open Image</a>
	<a class="${ns}-show-post-link entry" href="${data.sound.src}" target="_blank">Open Sound</a>
</div>`


/***/ }),

/***/ "./src/templates/list.tpl":
/*!********************************!*\
  !*** ./src/templates/list.tpl ***!
  \********************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = data => Player.sounds.map(sound =>
	`<div class="${ns}-list-item ${ns}-row ${sound.playing ? 'playing' : ''}" data-id="${sound.id}" draggable="true">
		<div class="${ns}-col ${ns}-truncate-text">
			<span title="${sound.title}">${sound.title}</span>
		</div>
		<div class="${ns}-col-auto ${ns}-item-menu-button">
			<i class="fa fa-angle-down">▼</i>
		</div>
	</div>`
).join('')

/***/ }),

/***/ "./src/templates/player.tpl":
/*!**********************************!*\
  !*** ./src/templates/player.tpl ***!
  \**********************************/
/*! no static exports found */
/***/ (function(module, exports) {

module.exports = 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">
	${Player.templates.list(data)}
</div>`

/***/ }),

/***/ "./src/templates/settings.tpl":
/*!************************************!*\
  !*** ./src/templates/settings.tpl ***!
  \************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

module.exports = data => {
	const settingsConfig = __webpack_require__(/*! settings */ "./src/settings.js");

	return settingsConfig.filter(setting => setting.showInSettings).map(function addSetting(setting) {
		let out = `<div class="${setting.isSubSetting ? `${ns}-col` : `${ns}-setting-header`}" ${setting.description ? `title="${setting.description}"` : ''}>
			${setting.title}
			${(setting.actions || []).map(action => `<a href="javascript;" class="${ns}-setting-action" data-handler="${action.handler}">${action.title}</a>`)}
		</div>`;

		if (setting.settings) {
			out += `<div class="${ns}-row ${ns}-sub-settings">`
				+ setting.settings.map(subSetting => {
					return addSetting({ ...setting, actions: null, settings: null, ...subSetting, isSubSetting: true })
				}).join('')
			+ `</div>`;

			return out;
		}

		let value = _get(Player.config, setting.property, setting.default);
		let clss = setting.class ? `class="${setting.class}"` : '';

		if (setting.format) {
			value = _get(Player, setting.format)(value);
		}

		let type = typeof value;

		setting.isSubSetting && (out += `<div class="${ns}-col">`);

		if (type === 'boolean') {
			out += `<input type="checkbox" ${clss} data-property="${setting.property}" ${value ? 'checked' : ''}></input>`;
		} else if (setting.showInSettings === 'textarea' || type === 'object') {
			if (setting.split) {
				value = value.join(setting.split);
			} else if (type === 'object') {
				value = JSON.stringify(value, null, 4);
			}
			out += `<textarea ${clss} data-property="${setting.property}">${value}</textarea>`;
		} else if (setting.options) {
			out += `<select ${clss} data-property="${setting.property}">`
				+ setting.options.map(option => `<option value="${option[0]}" ${value === option[0] ? 'selected' : ''}>${option[1]}</option>`)
			+ '</select>';
		} else {
			out += `<input type="text" ${clss} data-property="${setting.property}" value="${value}"></input>`;
		}

		setting.isSubSetting && (out += `</div><div class="${ns}-col" style="min-width: 100%"></div>`);
		return out;
	}).join('')
}

/***/ })

/******/ });