Greasy Fork is available in English.

4chan sounds player

Play that faggy music weeb boi

23.05.2020 itibariyledir. En son verisyonu görün.

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

/***/ })

/******/ });