// ==UserScript== // @name 4chan sounds player // @version 2.2.0 // @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 // @grant GM.xmlHttpRequest // @connect 4chan.org // @connect 4channel.org // @connect a.4cdn.org // @connect desu-usergeneratedcontent.xyz // @connect arch-img.b4k.co // @connect 4cdn.org // @connect a.pomf.cat // @connect files.catbox.moe // @connect share.dmca.gripe // @connect z.zz.ht // @connect too.lewd.se // @connect * // @run-at document-start // @downloadURL none // ==/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/api.js": /*!********************!*\ !*** ./src/api.js ***! \********************/ /*! no static exports found */ /***/ (function(module, exports) { const cache = {}; module.exports = { get }; async function get(url) { return new Promise(function (resolve, reject) { const headers = {}; if (cache[url]) { headers['If-Modified-Since'] = cache[url].lastModified; } GM.xmlHttpRequest({ method: 'GET', url, headers, responseType: 'json', onload: response => { if (response.status >= 200 && response.status < 300) { cache[url] = { lastModified: response.responseHeaders['last-modified'], response: response.response }; } resolve(response.status === 304 ? cache[url].response : response.response); }, onerror: reject }); }); } /***/ }), /***/ "./src/components/controls.js": /*!************************************!*\ !*** ./src/components/controls.js ***! \************************************/ /*! no static exports found */ /***/ (function(module, exports) { const progressBarStyleSheets = {}; 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', [`.${ns}-fullscreen-button`]: 'display.toggleFullScreen' }, 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('show', () => Player._hiddenWhilePolling && Player.controls.pollForLoading()); Player.on('hide', () => { Player._hiddenWhilePolling = !!Player._loadingPoll; Player.controls.stopPollingForLoading(); }); Player.on('rendered', () => { // Keep track of heavily updated elements. 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(progressBarStyleSheets[`.${ns}-seek-bar`] = document.createElement('style')); document.head.appendChild(progressBarStyleSheets[`.${ns}-volume-bar`] = document.createElement('style')); Player.controls.updateDuration(); Player.controls.updateVolume(); }); }, /** * Switching being playing and paused. */ togglePlay: function () { if (Player.audio.paused) { Player.play(); } else { Player.pause(); } }, /** * Start playback. */ play: async function (sound) { 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 = document.querySelector(`.${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; 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 = document.querySelector(`.${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(); document.querySelectorAll(`.${ns}-play-button .${ns}-play-button-display`).forEach(el => { el.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 = document.querySelector(`.${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; } const currentTime = toDuration(Player.audio.currentTime); const duration = toDuration(Player.audio.duration); document.querySelectorAll(`.${ns}-current-time`).forEach(el => el.innerHTML = currentTime); document.querySelectorAll(`.${ns}-duration`).forEach(el => el.innerHTML = 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 (progressBarStyleSheets[id]) { progressBarStyleSheets[id].innerHTML = `${id} .${ns}-current-bar:after { margin-right: ${-0.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' }, fullscreenchange: { [`.${ns}-media`]: 'display._handleFullScreenChange' }, drop: { [`#${ns}-container`]: 'display._handleDrop' } }, /** * 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 = createElement(` Sounds `); shortcuts.insertBefore(showIcon, document.getElementById('shortcut-settings')); 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); } // Create the main stylesheet. Player.display.updateStylesheet(); // Create the main player. For native threads put it in the threads to get free quote previews. const isThread = document.body.classList.contains('is_thread'); const parent = isThread && !isChanX && document.body.querySelector('.board') || document.body; Player.container = createElement(Player.templates.body(), parent); Player.trigger('rendered'); } 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. Player.stylesheet = Player.stylesheet || createElement('', document.head); Player.stylesheet.innerHTML = Player.templates.css(); }, /** * Change what view is being shown */ setViewStyle: function (style) { // Get the size and style prior to switching. const previousStyle = Player.config.viewStyle; const { width, height } = Player.container.getBoundingClientRect(); // Exit fullscreen before changing to a different view. if (style !== 'fullscreen') { document.fullscreenElement && document.exitFullscreen(); } // Change the style. Player.set('viewStyle', style); Player.container.setAttribute('data-view-style', style); // Try to reapply the pre change sizing unless it was fullscreen. if (previousStyle !== 'fullscreen' || style === 'fullscreen') { Player.position.resize(parseInt(width, 10), parseInt(height, 10)); } Player.trigger('view', style, previousStyle); }, /** * 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 loaded amount polling if it was paused. */ 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; await 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); } }, /** * Toggle the video/image and controls fullscreen state */ toggleFullScreen: async function () { if (!document.fullscreenElement) { // Make sure the player (and fullscreen contents) are visible first. if (Player.isHidden) { Player.show(); } Player.$(`.${ns}-media`).requestFullscreen(); } else if (document.exitFullscreen) { document.exitFullscreen(); } }, /** * Handle file/s being dropped on the player. */ _handleDrop: function (e) { e.preventDefault(); e.stopPropagation(); Player.playlist.addFromFiles(e.dataTransfer.files); }, /** * Handle the fullscreen state being changed */ _handleFullScreenChange: function () { if (document.fullscreenElement) { Player.display.setViewStyle('fullscreen'); document.querySelector(`.${ns}-image-link`).removeAttribute('href'); } else { if (Player.playing) { document.querySelector(`.${ns}-image-link`).href = Player.playing.image; } Player.playlist.restore(); } } }; /***/ }), /***/ "./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 (let 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. Player.events.addDelegatedListeners(Player.container, delegated); // Wire up undelegated events. Player.events.addUndelegatedListeners(document, undelegated); // Wire up audio events. for (let eventList of audio) { for (let evt in eventList) { Player.audio.addEventListener(evt, Player.events.getHandler(eventList[evt])); } } }); }, /** * Set delegated events listeners on a target */ addDelegatedListeners(target, events) { for (let evt in events) { target.addEventListener(evt, function (e) { let nodes = [ e.target ]; while (nodes[nodes.length - 1] !== target) { nodes.push(nodes[nodes.length - 1].parentNode); } for (let node of nodes) { for (let eventList of [].concat(events[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; } } } } } }); } }, /** * Set, or reset, directly bound events. */ addUndelegatedListeners: function (target, events) { for (let evt in events) { for (let eventList of [].concat(events[evt])) { for (let selector in eventList) { target.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 = { initialize: function () { Player.userTemplate.maintain(Player.footer, 'footerTemplate'); }, render: function () { if (Player.container) { Player.$(`.${ns}-footer`).innerHTML = Player.templates.footer(); } } }; /***/ }), /***/ "./src/components/header.js": /*!**********************************!*\ !*** ./src/components/header.js ***! \**********************************/ /*! no static exports found */ /***/ (function(module, exports) { module.exports = { initialize: function () { Player.userTemplate.maintain(Player.header, 'headerTemplate'); }, render: function () { if (Player.container) { Player.$(`.${ns}-header`).innerHTML = Player.templates.header(); } } }; /***/ }), /***/ "./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 + 0.05, 1); }, volumeDown: function () { Player.audio.volume = Math.max(Player.audio.volume - 0.05, 0); } }; /***/ }), /***/ "./src/components/minimised.js": /*!*************************************!*\ !*** ./src/components/minimised.js ***! \*************************************/ /*! no static exports found */ /***/ (function(module, exports) { module.exports = { _showingPIP: false, initialize: function () { if (isChanX) { // Create a reply element to gather the style from const a = createElement('', document.body); const style = document.defaultView.getComputedStyle(a); createElement(``, document.head); // Clean up the element. document.body.removeChild(a); // Set up the contents and maintain user template changes. Player.userTemplate.maintain(Player.minimised, 'chanXTemplate', [ 'chanXControls' ], [ 'show', 'hide' ]); } Player.on('rendered', Player.minimised.render); Player.on('show', Player.minimised.hidePIP); Player.on('hide', Player.minimised.showPIP); }, render: function () { if (Player.container && isChanX) { let container = document.querySelector(`.${ns}-chan-x-controls`); // Create the element if it doesn't exist. // Set the user template and control events on it to make all the buttons work. if (!container) { container = createElementBefore(``, document.querySelector('#shortcuts').firstElementChild); Player.events.addDelegatedListeners(container, { click: [ Player.userTemplate.delegatedEvents.click, Player.controls.delegatedEvents.click ] }); } if (Player.config.chanXControls === 'never' || Player.config.chanXControls === 'closed' && !Player.isHidden) { return container.innerHTML = ''; } // Render the contents. container.innerHTML = Player.userTemplate.build({ template: Player.config.chanXTemplate, sound: Player.playing, replacements: { 'prev-button': `
`, 'play-button': ` `, 'next-button': ` `, 'sound-current-time': `0:00`, 'sound-duration': `0:00` } }); } }, /** * Move the image to a picture in picture like thumnail. */ showPIP: function () { if (!Player.config.pip || !Player.playing || Player.minimised._showingPIP) { return; } Player.minimised._showingPIP = true; const image = document.querySelector(`.${ns}-image-link`); document.body.appendChild(image); image.classList.add(`${ns}-pip`); image.style.bottom = (Player.position.getHeaderOffset().bottom + 10) + 'px'; // Show the player again when the image is clicked. image.addEventListener('click', Player.show); }, /** * Move the image back to the player. */ hidePIP: function () { Player.minimised._showingPIP = false; const image = document.querySelector(`.${ns}-image-link`); Player.$(`.${ns}-media`).insertBefore(document.querySelector(`.${ns}-image-link`), Player.$(`.${ns}-controls`)); image.classList.remove(`${ns}-pip`); image.style.bottom = null; image.removeEventListener('click', Player.show); } }; /***/ }), /***/ "./src/components/playlist.js": /*!************************************!*\ !*** ./src/components/playlist.js ***! \************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { const { parseFiles, parseFileName } = __webpack_require__(/*! ../file_parser */ "./src/file_parser.js"); module.exports = { atRoot: [ 'add', 'remove' ], delegatedEvents: { click: { [`.${ns}-list-item`]: 'playlist.handleSelect' }, mousemove: { [`.${ns}-list-item`]: 'playlist.positionHoverImage' }, 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: { mouseenter: { [`.${ns}-list-item`]: 'playlist.updateHoverImage' }, mouseleave: { [`.${ns}-list-item`]: 'playlist.removeHoverImage' } }, initialize: function () { // Keep track of the last view style so we can return to it. Player.playlist._lastView = Player.config.viewStyle === 'playlist' || Player.config.viewStyle === 'image' ? Player.config.viewStyle : 'playlist'; Player.on('view', style => { // Focus the playing song when switching to the playlist. style === 'playlist' && Player.playlist.scrollToPlaying(); // Track state. if (style === 'playlist' || style === 'image') { Player.playlist._lastView = style; } }); // Update the UI when a new sound plays, and scroll to it. Player.on('playsound', sound => { Player.playlist.showImage(sound); Player.$all(`.${ns}-list-item.playing`).forEach(el => el.classList.remove('playing')); Player.$(`.${ns}-list-item[data-id="${Player.playing.id}"]`).classList.add('playing'); Player.playlist.scrollToPlaying('nearest'); }); // Reapply filters when they change Player.on('config:filters', Player.playlist.applyFilters); // Listen to anything that can affect the display of hover images Player.on('config:hoverImages', Player.playlist.setHoverImageVisibility); Player.on('menu-open', Player.playlist.setHoverImageVisibility); Player.on('menu-close', Player.playlist.setHoverImageVisibility); // Maintain changes to the user templates it's dependent values Player.userTemplate.maintain(Player.playlist, 'rowTemplate', [ 'shuffle' ]); }, /** * Render the playlist. */ render: function () { if (!Player.container) { return; } const container = Player.$(`.${ns}-list-container`); container.innerHTML = Player.templates.list(); Player.events.addUndelegatedListeners(document.body, Player.playlist.undelegatedEvents); Player.playlist.hoverImage = container.querySelector(`.${ns}-hover-image`); }, /** * Restore the last playlist or image view. */ restore: function () { Player.display.setViewStyle(Player.playlist._lastView || 'playlist'); }, /** * 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') || sound.type === 'video/webm'); try { const container = document.querySelector(`.${ns}-image-link`); const img = container.querySelector(`.${ns}-image`); const video = container.querySelector(`.${ns}-video`); img.src = ''; img.src = isVideo || thumb ? sound.thumb : sound.image; video.src = isVideo ? sound.image : undefined; if (Player.config.viewStyle !== 'fullscreen') { container.href = sound.image; } container.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); } 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 (sound, skipRender) { try { const id = sound.id; // Make sure the sound is an allowed host, not filtered, and not a duplicate. if (!Player.acceptedSound(sound) || Player.sounds.find(sound => sound.id === id)) { return; } // 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 => Player.compareIds(s.id, id) > 1); index < 0 && (index = Player.sounds.length); Player.sounds.splice(index, 0, sound); if (Player.container) { if (!skipRender) { // Add the sound to the playlist. const list = Player.$(`.${ns}-list-container`); let rowContainer = document.createElement('div'); rowContainer.innerHTML = Player.templates.list({ sounds: [ sound ] }); Player.events.addUndelegatedListeners(rowContainer, Player.playlist.undelegatedEvents); let row = rowContainer.children[0]; if (index < Player.sounds.length - 1) { const before = Player.$(`.${ns}-list-item[data-id="${Player.sounds[index + 1].id}"]`); list.insertBefore(row, before); } else { list.appendChild(row); } } // 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]', sound); console.error('[4chan sounds player]', err); } }, addFromFiles: function (files) { // Check each of the files for sounds. [ ...files ].forEach(file => { if (!file.type.startsWith('image') && file.type !== 'video/webm') { return; } const imageSrc = URL.createObjectURL(file); const type = file.type; let thumbSrc = imageSrc; // If it's not a webm just use the full image as the thumbnail if (file.type !== 'video/webm') { return _continue(); } // If it's a webm grab the first frame as the thumbnail const canvas = document.createElement('canvas'); const video = document.createElement('video'); const context = canvas.getContext('2d'); video.addEventListener('loadeddata', function () { context.drawImage(video, 0, 0); thumbSrc = canvas.toDataURL(); _continue(); }); video.src = imageSrc; function _continue() { parseFileName(file.name, imageSrc, null, thumbSrc).forEach(sound => Player.add({ ...sound, local: true, type })); } }); }, /** * 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); // Remove the item from the list. Player.$(`.${ns}-list-container`).removeChild(Player.$(`.${ns}-list-item[data-id="${sound.id}"]`)); Player.trigger('remove', sound); }, /** * Handle an playlist item being clicked. Either open/close the menu or play the sound. */ handleSelect: function (e) { // Ignore if a link was clicked. if (e.target.nodeName === 'A' || e.target.closest('a')) { return; } e.preventDefault(); const id = e.eventTarget.getAttribute('data-id'); const sound = id && Player.sounds.find(sound => sound.id === id); sound && Player.play(sound); }, /** * Read all the sounds from the thread again. */ refresh: function () { parseFiles(document.body); }, /** * Toggle the hoverImages setting */ toggleHoverImages: function (e) { e && e.preventDefault(); Player.set('hoverImages', !Player.config.hoverImages); }, /** * Only show the hover image with the setting enabled, no item menu open, and nothing being dragged. */ setHoverImageVisibility: function () { const container = Player.$(`.${ns}-list-container`); const hideImage = !Player.config.hoverImages || Player.playlist._dragging || container.querySelector(`.${ns}-item-menu`); container.classList[hideImage ? 'add' : 'remove'](`${ns}-hide-hover-image`); }, /** * Set the displayed hover image and reposition. */ updateHoverImage: function (e) { const id = e.currentTarget.getAttribute('data-id'); const sound = Player.sounds.find(sound => sound.id === id); Player.playlist.hoverImage.style.display = 'block'; Player.playlist.hoverImage.setAttribute('src', sound.thumb); Player.playlist.positionHoverImage(e); }, /** * Reposition the hover image to follow the cursor. */ positionHoverImage: function (e) { const { width, height } = Player.playlist.hoverImage.getBoundingClientRect(); const maxX = document.documentElement.clientWidth - width - 5; Player.playlist.hoverImage.style.left = (Math.min(e.clientX, maxX) + 5) + 'px'; Player.playlist.hoverImage.style.top = (e.clientY - height - 10) + 'px'; }, /** * Hide the hover image when nothing is being hovered over. */ removeHoverImage: function () { Player.playlist.hoverImage.style.display = 'none'; }, /** * Start dragging a playlist item. */ handleDragStart: function (e) { Player.playlist._dragging = e.eventTarget; Player.playlist.setHoverImageVisibility(); 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')); }, /** * Swap a playlist item when it's dragged over another item. */ handleDragEnter: function (e) { if (!Player.playlist._dragging) { return; } 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'); }, /** * Start dragging a playlist item. */ handleDragEnd: function (e) { if (!Player.playlist._dragging) { return; } e.preventDefault(); delete Player.playlist._dragging; e.eventTarget.classList.remove(`${ns}-dragging`); Player.playlist.setHoverImageVisibility(); }, /** * Scroll to the playing item, unless there is an open menu in the playlist. */ scrollToPlaying: function (type = 'center') { if (Player.$(`.${ns}-list-container .${ns}-item-menu`)) { return; } const playing = Player.$(`.${ns}-list-item.playing`); playing && playing.scrollIntoView({ block: type }); }, /** * Remove any user filtered items from the playlist. */ applyFilters: function () { Player.sounds.filter(sound => !Player.acceptedSound(sound)).forEach(Player.playlist.remove); } }; /***/ }), /***/ "./src/components/position.js": /*!************************************!*\ !*** ./src/components/position.js ***! \************************************/ /*! no static exports found */ /***/ (function(module, exports) { module.exports = { delegatedEvents: { mousedown: { [`.${ns}-header`]: '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, .backlink_container article').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 || Player.config.viewStyle === 'fullscreen') { 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'; // Which element to change the height of depends on the view being displayed. 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`) : Player.config.viewStyle === 'threads' ? Player.$(`.${ns}-threads`) : null; if (!heightElement) { return; } const offset = Player.container.getBoundingClientRect().height - heightElement.getBoundingClientRect().height; heightElement.style.height = (height - offset) + 'px'; }, /** * Handle the user grabbing the header. */ initMove: function (e) { e.preventDefault(); Player.$(`.${ns}-header`).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 () { document.documentElement.removeEventListener('mousemove', Player.position.doMove, false); document.documentElement.removeEventListener('mouseup', Player.position.stopMove, false); Player.$(`.${ns}-header`).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 = { atRoot: [ 'set' ], delegatedEvents: { click: { [`.${ns}-settings .${ns}-heading-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 () { if (Player.container) { 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 = createElement(``, document.body); 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 && Player.set(setting.property, setting.default, { bypassSave: true, bypassRender: true }); }); // Clean up the element. document.body.removeChild(div); // Updated the stylesheet if it exists. Player.stylesheet && Player.display.updateStylesheet(); // Re-render the settings if needed. Player.settings.render(); }, /** * Update a setting. */ set: function (property, value, { bypassSave, bypassRender, silent } = {}) { const previousValue = _get(Player.config, property); _set(Player.config, property, value); !silent && Player.trigger('config', property, value, previousValue); !silent && Player.trigger('config:' + property, value, previousValue); !bypassSave && Player.settings.save(); !bypassRender && Player.settings.findDefault(property).showInSettings && Player.settings.render(); }, /** * Reset a setting to the default value */ reset: function (property) { let settingConfig = Player.settings.findDefault(property); Player.set(property, settingConfig.default); }, /** * 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; }, {}); // Show the playlist or image view on load, whichever was last shown. settings.viewStyle = Player.playlist._lastView; // Store the player version with the settings. settings.VERSION = "2.2.0"; // 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) { Player.set(setting.property, userVal, { bypassSave: true, silent: true }); } }); } 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); } }, /** * Find a setting in the default configuration. */ findDefault: function (property) { let settingConfig; settingsConfig.find(function (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; }); return settingConfig || { property }; }, /** * Toggle whether the player or settings are displayed. */ toggle: function (e) { e && e.preventDefault(); // Blur anything focused so the change is applied. let focused = Player.$(`.${ns}-settings :focus`); focused && focused.blur(); if (Player.config.viewStyle === 'settings') { Player.playlist.restore(); } else { Player.display.setViewStyle('settings'); } }, /** * Handle the user making a change in the settings view. */ handleChange: function (e) { try { const input = e.eventTarget; const property = input.getAttribute('data-property'); if (!property) { return; } let settingConfig = Player.settings.findDefault(property); // 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. Player.set(property, newValue, { bypassRender: true }); // Update the stylesheet reflect any changes. if (settingConfig.updateStylesheet) { Player.display.updateStylesheet(); } } // 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); } }, /** * Converts a key event in an input to a string representation set as the input value. */ handleKeyChange: function (e) { e.preventDefault(); if (e.key === 'Shift' || e.key === 'Control' || e.key === 'Meta') { return; } e.eventTarget.value = Player.hotkeys.stringifyKey(e); }, /** * Handle an action link next to a heading being clicked. */ handleAction: function (e) { e.preventDefault(); const property = e.eventTarget.getAttribute('data-property'); const handlerName = e.eventTarget.getAttribute('data-handler'); const handler = _get(Player, handlerName); handler && handler(property); } }; /***/ }), /***/ "./src/components/threads.js": /*!***********************************!*\ !*** ./src/components/threads.js ***! \***********************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { const { parseFileName } = __webpack_require__(/*! ../file_parser */ "./src/file_parser.js"); const { get } = __webpack_require__(/*! ../api */ "./src/api.js"); const boardsURL = 'https://a.4cdn.org/boards.json'; const catalogURL = 'https://a.4cdn.org/%s/catalog.json'; module.exports = { boardList: null, soundThreads: null, displayThreads: {}, selectedBoards: Board ? [ Board ] : [ 'a' ], showAllBoards: false, delegatedEvents: { click: { [`.${ns}-fetch-threads-link`]: 'threads.fetch', [`.${ns}-all-boards-link`]: 'threads.toggleBoardList' }, keyup: { [`.${ns}-threads-filter`]: e => Player.threads.filter(e.eventTarget.value) }, change: { [`.${ns}-threads input[type=checkbox]`]: 'threads.toggleBoard' } }, initialize: function () { Player.threads.hasParser = is4chan && typeof Parser !== 'undefined'; // If the native Parser hasn't been intialised chuck customSpoiler on it so we can call it for threads. // You shouldn't do things like this. We can fall back to the table view if it breaks though. if (Player.threads.hasParser && !Parser.customSpoiler) { Parser.customSpoiler = {}; } Player.on('show', Player.threads._initialFetch); Player.on('view', Player.threads._initialFetch); Player.on('rendered', Player.threads.afterRender); Player.on('config:threadsViewStyle', Player.threads.render); }, /** * Fetch the threads when the threads view is opened for the first time. */ _initialFetch: function () { if (Player.container && Player.config.viewStyle === 'threads' && Player.threads.boardList === null) { Player.threads.fetchBoards(true); } }, render: function () { if (Player.container) { Player.$(`.${ns}-threads`).innerHTML = Player.templates.threads(); Player.threads.afterRender(); } }, /** * Render the threads and apply the board styling after the view is rendered. */ afterRender: function () { const threadList = Player.$(`.${ns}-thread-list`); if (threadList) { const bodyStyle = document.defaultView.getComputedStyle(document.body); threadList.style.background = bodyStyle.backgroundColor; threadList.style.backgroundImage = bodyStyle.backgroundImage; threadList.style.backgroundRepeat = bodyStyle.backgroundRepeat; threadList.style.backgroundPosition = bodyStyle.backgroundPosition; } Player.threads.renderThreads(); }, /** * Render just the threads. */ renderThreads: function () { if (!Player.threads.hasParser || Player.config.threadsViewStyle === 'table') { Player.$(`.${ns}-threads-body`).innerHTML = Player.templates.threadList(); } else { try { const list = Player.$(`.${ns}-thread-list`); for (let board in Player.threads.displayThreads) { // Create a board title const boardConf = Player.threads.boardList.find(boardConf => boardConf.board === board); const boardTitle = `/${boardConf.board}/ - ${boardConf.title}`; createElement(`Thread | Subject | Replies/Images | Started | Updated |
---|---|---|---|---|