// ==UserScript==
// @name 4chan sounds player
// @version 1.5.5
// @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' },
pause: { [`.${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: async 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];
}
const video = Player.$(`.${ns}-video`);
video.removeEventListener('loadeddata', Player.controls.playOnceLoaded);
// 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;
await Player.trigger('playsound', sound);
}
// If there's a video wait for it and the sound to load before playing.
if (Player.playlist.isVideo && (video.readyState < 3 || Player.audio.readyState < 3)) {
video.addEventListener('loadeddata', Player.controls._playOnceLoaded);
Player.audio.addEventListener('loadeddata', Player.controls._playOnceLoaded);
} else {
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);
}
},
/**
* Handler to start playback once the video and audio are both loaded.
*/
_playOnceLoaded: function () {
const video = Player.$(`.${ns}-video`);
if (video.readyState > 2 && Player.audio.readyState > 2) {
video.removeEventListener('loadeddata', Player.controls._playOnceLoaded);
Player.audio.removeEventListener('loadeddata', Player.controls._playOnceLoaded);
Player.audio.play();
}
},
/**
* Pause playback.
*/
pause: function () {
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 (Player.audio.currentTime < video.duration) {
video.currentTime = Player.audio.currentTime;
}
if (paused) {
video.pause();
} else {
video.play();
}
}
}
},
/**
* Poll for how much has loaded. I know there's the progress event but it unreliable.
*/
pollForLoading: function () {
Player._loadingPoll = Player._loadingPoll || setInterval(Player.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) {
await handler(...data);
}
},
/**
* 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 = {
delegatedEvents: {
click: {
[`.${ns}-playing-jump-link`]: () => Player.playlist.scrollToPlaying('center')
}
},
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 => {
// Update the image.
Player.playlist.showImage(sound);
// Update the playing attribute.
Player.$all(`.${ns}-list-item.playing`).forEach(el => el.classList.remove('playing'));
Player.$(`.${ns}-list-item[data-id="${Player.playing.id}"]`).classList.add('playing');
// Scroll to the sound.
Player.playlist.scrollToPlaying('nearest');
});
},
/**
* 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 {
const img = Player.$(`.${ns}-image`);
const video = Player.$(`.${ns}-video`);
img.src = '';
img.src = isVideo || thumb ? sound.thumb : sound.image;
video.src = isVideo ? sound.image : null;
Player.$(`.${ns}-image-link`).href = sound.image;
Player.$(`.${ns}-image-link`).classList[isVideo ? 'add' : '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;
},
scrollToPlaying: function (type = 'center') {
// Avoid scrolling if there's a menu open. That would be quite rude.
if (Player.$(`.${ns}-item-menu`)) {
return;
}
const playing = Player.$(`.${ns}-list-item.playing`);
playing && playing.scrollIntoView({ block: type });
}
};
/***/ }),
/***/ "./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, and post width limiting, 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);
if (Player.config.limitPostWidths) {
Player.position.setPostWidths();
window.addEventListener('scroll', Player.position.setPostWidths);
}
});
// Remove post width limiting when the player is hidden.
Player.on('hide', function () {
Player.position.setPostWidths();
window.removeEventListener('scroll', Player.position.setPostWidths);
});
// Reapply the post width limiting config values when they're changed.
Player.on('config', prop => {
if (prop === 'limitPostWidths' || prop === 'minPostWidth') {
window.removeEventListener('scroll', Player.position.setPostWidths);
Player.position.setPostWidths();
if (Player.config.limitPostWidths) {
window.addEventListener('scroll', Player.position.setPostWidths);
}
}
});
// Remove post width limit from inline quotes
new MutationObserver(function () {
document.querySelectorAll('#hoverUI .postContainer, .inline .postContainer').forEach(post => {
post.style.maxWidth = null;
post.style.minWidth = null;
})
}).observe(document.body, {
childList: true,
subtree: true
});
},
/**
* Applies a max width to posts next to the player so they don't get hidden behind it.
*/
setPostWidths: function () {
const offset = (document.documentElement.clientWidth - Player.container.offsetLeft) + 10;
const selector = is4chan ? '.thread > .postContainer' : '.posts > article.post';
const enabled = !Player.isHidden && Player.config.limitPostWidths;
const startY = Player.container.offsetTop;
const endY = Player.container.getBoundingClientRect().height + startY;
document.querySelectorAll(selector).forEach(post => {
const rect = enabled && post.getBoundingClientRect();
const limitWidth = enabled && rect.top + rect.height > startY && rect.top < endY;
post.style.maxWidth = limitWidth ? `calc(100% - ${offset}px)` : null;
post.style.minWidth = limitWidth && Player.config.minPostWidth ? `${Player.config.minPostWidth}` : null;
})
},
/**
* 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';
if (Player.config.limitPostWidths) {
Player.position.setPostWidths();
}
},
/**
* 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, setting) {
if (setting.settings) {
setting.settings.forEach(subSetting => {
let _setting = { ...setting, ...subSetting };
_set(config, _setting.property, _setting.default);
});
return config;
}
return _set(config, setting.property, setting.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) {
setting.settings.forEach(subSetting => _handleSetting(settings, {
property: setting.property,
default: setting.default,
...subSetting
}));
} else {
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);
settingsConfig.forEach(function _handleSetting(setting) {
if (setting.settings) {
return setting.settings.forEach(subSetting => _handleSetting({
property: setting.property,
default: setting.default,
...subSetting
}));
}
const userVal = _get(settings, setting.property);
if (userVal !== undefined) {
_set(Player.config, setting.property, userVal);
}
});
} catch(e) {
console.error(e);
return;
}
} 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('.');
const lastProp = props.pop();
const parent = props.reduce((obj, k) => obj && obj[k], object);
return parent && Object.prototype.hasOwnProperty.call(parent, lastProp)
? parent[lastProp]
: 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;
};
/***/ }),
/***/ "./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),
$all: (...args) => Player.container && Player.container.querySelectorAll(...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 => `.${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-header.${ns}-has-description {
cursor: help;
}
.${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,
settings: [ { title: 'Enabled' } ]
},
{
property: 'pauseOnHide',
default: true,
title: 'Pause on hide',
description: 'Pause the player when it\'s hidden.',
showInSettings: true,
settings: [ { title: 'Enabled' } ]
},
{
property: 'limitPostWidths',
title: 'Limit Post Width',
description: 'Limit the width of posts so they aren\'t hidden under the player.',
showInSettings: true,
settings: [
{
property: 'limitPostWidths',
title: 'Enabled',
default: false
},
{
property: 'minPostWidth',
title: 'Minimum Width',
default: '50%'
}
]
},
{
title: 'Keybinds',
showInSettings: true,
description: 'Enable keyboard shortcuts.',
format: 'hotkeys.stringifyKey',
parse: 'hotkeys.parseKey',
class: `${ns}-key-input`,
property: 'hotkey_bindings',
settings: [
{
property: 'hotkeys',
default: 'open',
handler: 'hotkeys.apply',
title: 'Enable',
format: null,
parse: null,
class: null,
options: {
always: 'Always',
open: 'Only with the player open',
never: 'Never'
}
},
{
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.scrollToPlaying',
title: 'Jump To Playing',
keyHandler: 'playlist.scrollToPlaying',
default: { key: '' }
},
{
property: 'hotkey_bindings.toggleHoverImages',
title: 'Toggle Hover Images',
keyHandler: 'playlist.toggleHoverImages',
default: { key: '' }
}
]
},
{
property: 'footerTemplate',
title: 'Footer Contents',
default: '%p / %t sounds\n<div style="float: right; margin-right: .5rem">\n\tplayinglink\n\tpostlink:"Post"\n\timagelink:"Image"\n\tsoundlink:"Sound"\n</div>',
description: 'What the footer displays, with the following replacements. The text for links can be set using the format postlink:"text".\n'
+ '%p - Playing index.\n'
+ '%t - Total sound count.\n'
+ 'playinglink - Jump to the current sound in the playlist.\n'
+ 'postlink - Jump to the current sounds post in the thread.\n'
+ 'imagelink - Open the current sounds image in a new tab.\n'
+ 'soundlink - Open the current sounds source in a new tab.',
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'
}
]
}
];
/***/ }),
/***/ "./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(/%v/g, "1.5.5")
.replace(/%p/g, Player.currentIndex || 0)
.replace(/%t/g, Player.sounds.length)
.replace(/(playing|post|image|sound)link(?:\:"([^"]+)")?/g, function (full, type, text) {
if (!Player.playing) {
return '';
}
const href = {
playing: 'javascript:;',
post: '#' + (is4chan ? 'p' : '') + Player.playing.id,
image: Player.playing.image,
sound: Player.playing.src
}[type];
const attrs = {
playing: `class="${ns}-playing-jump-link"`,
image: 'target="_blank"',
sound: 'target="_blank"'
}[type];
const defaultText = type[0].toUpperCase() + type.slice(1);
return Player.playing
? `<a href="${href}" ${attrs || ''}>${text || defaultText}</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) {
const desc = setting.description;
let out = `<div class="${setting.isSubSetting ? `${ns}-col` : `${ns}-setting-header`} ${desc ? `${ns}-has-description` : ''}" ${desc ? `title="${desc.replace(/"/g, '"')}"` : ''}>
${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,
description: 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' : ''} style="margin-bottom: .25rem"></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}" style="margin-bottom: .25rem">`
+ Object.keys(setting.options).map(k => `<option value="${k}" ${value === k ? 'selected' : ''}>${setting.options[k]}</option>`).join('')
+ '</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('')
}
/***/ })
/******/ });