// ==UserScript== // @name 4chan sounds player // @version 0.2.1 // @namespace rccom // @description Play that faggy music weeb boi // @author RCC // @match *://boards.4chan.org/* // @match *://boards.4channel.org/* // @grant GM.getValue // @grant GM.setValue // @run-at document-start // @downloadURL none // ==/UserScript== /** * @license * Lodash (Custom Build) lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE * Build: `lodash include="template,get,set" --production` */ ;(function(){function t(t,e,r){switch(r.length){case 0:return t.call(e);case 1:return t.call(e,r[0]);case 2:return t.call(e,r[0],r[1]);case 3:return t.call(e,r[0],r[1],r[2])}return t.apply(e,r)}function e(t,e){for(var r=-1,n=null==t?0:t.length,o=Array(n);++r=t}function L(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}function P(t){return null!=t&&typeof t=="object"}function T(t){return!(!P(t)||"[object Object]"!=h(t))&&(t=kt(t),null===t||(t=St.call(t,"constructor")&&t.constructor,typeof t=="function"&&t instanceof t&&At.call(t)==$t))}function B(t){return typeof t=="symbol"||P(t)&&"[object Symbol]"==h(t)}function C(t){return null==t?"":j(t)}function M(t){if(k(t))t=f(t);else if(w(t)){ var e,r=[];for(e in Object(t))St.call(t,e)&&"constructor"!=e&&r.push(e);t=r}else t=Tt(t);return t}function D(t){if(k(t))t=f(t,true);else if(L(t)){var e,r=w(t),n=[];for(e in t)("constructor"!=e||!r&&St.call(t,e))&&n.push(e);t=n}else{if(e=[],null!=t)for(r in Object(t))e.push(r);t=e}return t}function N(t){return function(){return t}}function W(t){return t}function q(){return false}var V,G=1/0,H=/\b__p\+='';/g,J=/\b(__p\+=)''\+/g,K=/(__e\(.*?\)|\b__t\))\+'';/g,Q=/[&<>"']/g,X=RegExp(Q.source),Y=/<%=([\s\S]+?)%>/g,Z=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,tt=/^\w*$/,et=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,rt=/\\(\\)?/g,nt=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,ot=/^\[object .+?Constructor\]$/,ut=/^(?:0|[1-9]\d*)$/,ct=/($^)/,it=/['\n\r\u2028\u2029\\]/g,at={}; at["[object Float32Array]"]=at["[object Float64Array]"]=at["[object Int8Array]"]=at["[object Int16Array]"]=at["[object Int32Array]"]=at["[object Uint8Array]"]=at["[object Uint8ClampedArray]"]=at["[object Uint16Array]"]=at["[object Uint32Array]"]=true,at["[object Arguments]"]=at["[object Array]"]=at["[object ArrayBuffer]"]=at["[object Boolean]"]=at["[object DataView]"]=at["[object Date]"]=at["[object Error]"]=at["[object Function]"]=at["[object Map]"]=at["[object Number]"]=at["[object Object]"]=at["[object RegExp]"]=at["[object Set]"]=at["[object String]"]=at["[object WeakMap]"]=false; var lt,ft={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},st=typeof global=="object"&&global&&global.Object===Object&&global,pt=typeof self=="object"&&self&&self.Object===Object&&self,_t=st||pt||Function("return this")(),ht=typeof exports=="object"&&exports&&!exports.nodeType&&exports,bt=ht&&typeof module=="object"&&module&&!module.nodeType&&module,yt=bt&&bt.exports===ht,gt=yt&&st.process;t:{try{lt=gt&>.binding&>.binding("util");break t}catch(t){}lt=void 0}var jt=lt&<.isTypedArray,vt=function(t){ return function(e){return null==t?V:t[e]}}({"&":"&","<":"<",">":">",'"':""","'":"'"}),dt=Array.prototype,mt=Object.prototype,Ot=_t["__core-js_shared__"],At=Function.prototype.toString,St=mt.hasOwnProperty,wt=function(){var t=/[^.]+$/.exec(Ot&&Ot.keys&&Ot.keys.IE_PROTO||"");return t?"Symbol(src)_1."+t:""}(),xt=mt.toString,$t=At.call(Object),Et=RegExp("^"+At.call(St).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),zt=yt?_t.Buffer:V,Ft=_t.Symbol,kt=u(Object.getPrototypeOf,Object),Rt=mt.propertyIsEnumerable,It=dt.splice,Ut=Ft?Ft.toStringTag:V,Lt=function(){ try{var t=O(Object,"defineProperty");return t({},"",{}),t}catch(t){}}(),Pt=zt?zt.isBuffer:V,Tt=u(Object.keys,Object),Bt=Math.max,Ct=Date.now,Mt=O(_t,"Map"),Dt=O(Object,"create"),Nt=Ft?Ft.prototype:V,Wt=Nt?Nt.toString:V;c.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:Y,variable:"",imports:{_:c}},i.prototype.clear=function(){this.__data__=Dt?Dt(null):{},this.size=0},i.prototype.delete=function(t){return t=this.has(t)&&delete this.__data__[t],this.size-=t?1:0,t}, i.prototype.get=function(t){var e=this.__data__;return Dt?(t=e[t],"__lodash_hash_undefined__"===t?V:t):St.call(e,t)?e[t]:V},i.prototype.has=function(t){var e=this.__data__;return Dt?e[t]!==V:St.call(e,t)},i.prototype.set=function(t,e){var r=this.__data__;return this.size+=this.has(t)?0:1,r[t]=Dt&&e===V?"__lodash_hash_undefined__":e,this},a.prototype.clear=function(){this.__data__=[],this.size=0},a.prototype.delete=function(t){var e=this.__data__;return t=p(e,t),!(0>t)&&(t==e.length-1?e.pop():It.call(e,t,1), --this.size,true)},a.prototype.get=function(t){var e=this.__data__;return t=p(e,t),0>t?V:e[t][1]},a.prototype.has=function(t){return-1n?(++this.size,r.push([t,e])):r[n][1]=e,this},l.prototype.clear=function(){this.size=0,this.__data__={hash:new i,map:new(Mt||a),string:new i}},l.prototype.delete=function(t){return t=m(this,t).delete(t),this.size-=t?1:0,t},l.prototype.get=function(t){return m(this,t).get(t)},l.prototype.has=function(t){ return m(this,t).has(t)},l.prototype.set=function(t,e){var r=m(this,t),n=r.size;return r.set(t,e),this.size+=r.size==n?0:1,this};var qt=function(t){var e=0,r=0;return function(){var n=Ct(),o=16-(n-r);if(r=n,0o?V:u,o=1),e=Object(e);++n { return _.set(settings, settingConfig.property, settingConfig.default); }, {}), $: (...args) => Player.container.querySelector(...args), // The templates are setup at initialization. templates: {}, _templates: { css: "#<%= ns %>-container {\n\tposition: fixed;\n\tbackground: <%= data.colors.background %>;\n\tborder: 1px solid <%= data.colors.border %>;\n\tdisplay: relative;\n\tmin-height: 200px;\n\tmin-width: 100px;\n}\n.<%= ns %>-show-settings .<%= ns %>-player {\n\tdisplay: none;\n}\n.<%= ns %>-setting {\n\tdisplay: none;\n}\n.<%= ns %>-settings {\n\tdisplay: none;\n\tpadding: .25rem;\n}\n.<%= ns %>-show-settings .<%= ns %>-settings {\n\tdisplay: block;\n}\n.<%= ns %>-settings .<%= ns %>-setting-header {\n\tfont-weight: 600;\n\tmargin-top: 0.25rem;\n}\n.<%= ns %>-settings textarea {\n\tborder: solid 1px <%= data.colors.border %>;\n\tmin-width: 100%;\n\tmin-height: 4rem;\n\tbox-sizing: border-box;\n}\n.<%= ns %>-title {\n\tcursor: grab;\n\ttext-align: center;\n\tborder-bottom: solid 1px <%= data.colors.border %>;\n\tpadding: .25rem 0;\n}\nhtml.fourchan-x .<%= ns %>-title a {\n\tfont-size: 0;\n\tvisibility: hidden;\n\tmargin: 0 0.15rem;\n}\nhtml.fourchan-x .<%= ns %>-title .fa-repeat.fa-repeat-one::after {\n\tcontent: '1';\n\tfont-size: .5rem;\n\tvisibility: visible;\n\tmargin-left: -1px;\n}\n.<%= ns %>-image-link {\n\ttext-align: center;\n\tdisplay: flex;\n\tjustify-items: center;\n\tjustify-content: center;\n\tborder-bottom: solid 1px <%= data.colors.border %>;\n}\n.<%= ns %>-playlist-view .<%= ns %>-image-link {\n\theight: 125px !important;\n}\n.<%= ns %>-expanded-view .<%= ns %>-image-link {\n\theight: auto ;\n\tmin-height: 125px;\n}\n.<%= ns %>-image-link .<%= ns %>-video {\n\tdisplay: none;\n}\n.<%= ns %>-image-link.<%= ns %>-show-video .<%= ns %>-video {\n\tdisplay: block;\n}\n.<%= ns %>-image-link.<%= ns %>-show-video .<%= ns %>-image {\n\tdisplay: none;\n}\n.<%= ns %>-image, .<%= ns %>-video {\n\theight: 100%;\n\twidth: 100%;\n\tobject-fit: contain;\n}\n.<%= ns %>-audio {\n\twidth: 100%;\n}\n.<%= ns %>-list-container {\n\toverflow: auto;\n}\n.<%= ns %>-expanded-view .<%= ns %>-list-container {\n\tdisplay: none;\n}\n.<%= ns %>-list {\n\tdisplay: grid;\n\tlist-style-type: none;\n\tpadding: 0;\n\tmargin: 0;\n}\n.<%= ns %>-list-item {\n\tlist-style-type: none;\n\tpadding: 0.15rem 0.25rem;\n\twhite-space: nowrap;\n\tcursor: pointer;\n}\n.<%= ns %>-list-item.playing {\n\tbackground: <%= data.colors.playing %> !important;\n}\n.<%= ns %>-list-item:nth-child(n) {\n\tbackground: <%= data.colors.odd_row %>;\n}\n.<%= ns %>-list-item:nth-child(2n) {\n\tbackground: <%= data.colors.even_row %>;\n}\n.<%= ns %>-footer {\n\tpadding: .15rem .25rem;\n\tborder-top: solid 1px <%= data.colors.border %>;\n}\n.<%= ns %>-expander {\n\tposition: absolute;\n\tbottom: 0px;\n\tright: 0px;\n\theight: .75rem;\n\twidth: .75rem;\n\tcursor: se-resize;\n\tbackground: linear-gradient(to bottom right, rgba(0,0,0,0), rgba(0,0,0,0) 50%, <%= data.colors.expander %> 55%, <%= data.colors.expander %> 100%)\n}\n.<%= ns %>-expander:hover {\n\tbackground: linear-gradient(to bottom right, rgba(0,0,0,0), rgba(0,0,0,0) 50%, <%= data.colors.expander_hover %> 55%, <%= data.colors.expander_hover %> 100%)\n}", body: "
-container\" class=\"<%= ns %>-<%= data.playlist ? 'playlist' : 'extended' %>-view\" style=\"top: 100px; left: 100px; width: 350px; display: none;\">\n\t
-title\" style=\"display: flex; flex-wrap: wrap; justify-content: between;\">\n\t\t<%= Player.templates.header({ data }) %>\n\t
\n\t
-player\">\n\t\t-image-link\" style=\"height: 128px\" target=\"_blank\">\n\t\t\t-image\">\n\t\t\t\n\t\t\n\t\t
-controls\">\n\t\t\t
-play-pause <%=ns %>-<%= !Player.audio || Player.audio.paused ? 'play' : 'pause' %>>\">
\n\t\t\t
-seekbar\">
\n\t\t\t
-duration\">
\n\t\t\t
-volume\">
\n\t\t
\n\t\t\n\t\t
-list-container\" style=\"height: 100px\">\n\t\t\t
    -list\">\n\t\t\t\t<%= Player.templates.list({ data }) %>\n\t\t\t
\n\t\t
\n\t\t
-footer\">\n\t\t\t-count\">0 sounds\n\t\t\t
-expander\">
\n\t\t
\n\t
\n\t
-settings\">\n\t\t<%= Player.templates.settings({ data }) %>\n\t
\n
", header: "
\n\t<% Object.keys(headerOptions).forEach(key => { %>\n\t\t<% let option = headerOptions[key][data[key]] || headerOptions[key][0]; %>\n\t\t-<%= key %>-button fa <%= option.class %>\" title=\"<%= option.title %>\" href=\"javascript;\">\n\t\t\t<%= option.text %>\n\t\t\n\t<% }) %>\n
\n\t<%= Player.playing ? Player.playing.title : '4chan Sounds' %>\n
\n
\n\t-config-button fa fa-wrench\" title=\"Settings\" href=\"javascript;\">Settings\n\t-close-button fa fa-times\" href=\"javascript;\">X\n
", list: "<% Player.sounds.forEach(sound => { %>\n\t
  • -list-item <%= sound.playing ? 'playing' : '' %>\" data-id=\"<%= sound.id %>\">\n\t\t<%= sound.title %>\n\t
  • \n<% }) %>", settings: "<% settingsConfig.forEach(setting => { %>\n\t<% if (setting.showInSettings) { %>\n\t\t
    -setting-header\" <%= setting.description ? `title=\"${setting.description}\"` : '' %>><%= setting.title %>
    \n\t\t<% if (typeof setting.default === 'boolean') { %>\n\t\t\t\" <%= _.get(data, setting.property, false) ? 'checked' : '' %>>\n\t\t<% } else if (Array.isArray(setting.default)) { %>\n\t\t\t\n\t\t<% } else { %>\n\t\t\t\" value=\"<%= _.get(data, setting.property, '') %>\">\n\t\t<% } %>\n\t<% } %>\n<% }); %>" }, events: { click: { [`.${ns}-close-button`]: 'hide', [`.${ns}-config-button`]: 'toggleSettings', [`.${ns}-shuffle-button`]: 'toggleShuffle', [`.${ns}-repeat-button`]: 'toggleRepeat', [`.${ns}-playlist-button`]: 'togglePlaylist', [`.${ns}-list`]: function (e) { const id = e.target.getAttribute('data-id'); const sound = id && Player.sounds.find(function (sound) { return sound.id === '' + id; }); sound && Player.play(sound); } }, mousedown: { [`.${ns}-title`]: 'initMove', [`.${ns}-expander`]: 'initResize' }, focusout: { [`.${ns}-settings input, .${ns}-settings textarea`]: 'handleSettingChange' }, change: { [`.${ns}-settings input[type=checkbox]`]: 'handleSettingChange' } }, initialize: async function () { try { await Player.loadSettings(); Player.sounds = [ ]; Player.playOrder = [ ]; // Set up the template functions. for (let name in Player._templates) { Player.templates[name] = _.template(Player._templates[name]); } if (isChanX) { Player.initChanX() } else { document.querySelectorAll('#settingsWindowLink, #settingsWindowLinkBot').forEach(function (link) { const bracket = document.createTextNode('] ['); const showLink = document.createElement('a'); showLink.innerHTML = 'Sounds'; showLink.href = 'javascript;'; link.parentNode.insertBefore(showLink, link); link.parentNode.insertBefore(bracket, link); showLink.addEventListener('click', Player.toggleDisplay); }); } Player.render(); } catch (err) { _logError('There was an error intiaizing the sound player. Please check the console for details.'); console.error('[4chan sounds player]'); // Can't recover so throw this error. throw err; } }, initChanX: function () { if (Player._initedChanX) { return; } Player._initedChanX = true; const shortcuts = document.getElementById('shortcuts'); const showIcon = document.createElement('span'); shortcuts.insertBefore(showIcon, document.getElementById('shortcut-settings')); const attrs = { id: 'shortcut-sounds', class: 'shortcut brackets-wrap', 'data-index': 0 }; for (let attr in attrs) { showIcon.setAttribute(attr, attrs[attr]); } showIcon.innerHTML = 'Sounds'; showIcon.querySelector('a').addEventListener('click', Player.toggleDisplay); }, saveSettings: function () { try { return GM.setValue(ns + '.settings', JSON.stringify(Player.settings)); } catch (err) { _logError('There was an error saving the sound player settings. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, loadSettings: async function () { try { let settings = await GM.getValue(ns + '.settings'); if (!settings) { return; } try { settings = JSON.parse(settings); } catch(e) { return; } function _mix (to, from) { for (let key in from) { if (from[key] && typeof from[key] === 'object' && !Array.isArray(from[key])) { to[key] || (to[key] = {}); _mix(to[key], from[key]); } else { to[key] = from[key]; } } } _mix(Player.settings, settings); } catch (err) { _logError('There was an error loading the sound player settings. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, _tplOptions: function () { return { data: Player.settings }; }, render: async function () { try { if (Player.container) { document.body.removeChild(Player.container); document.head.removeChild(Player.stylesheet); } // Insert the stylesheet Player.stylesheet = document.createElement('style'); Player.stylesheet.innerHTML = Player.templates.css(Player._tplOptions()); document.head.appendChild(Player.stylesheet); // Create the main player const el = document.createElement('div'); el.innerHTML = Player.templates.body(Player._tplOptions()); Player.container = el.querySelector(`#${ns}-container`); document.body.appendChild(Player.container); // Keep track of the audio element Player.audio = Player.$(`.${ns}-audio`); // Wire up event listeners. for (let evt in Player.events) { Player.container.addEventListener(evt, function (e) { for (let selector in Player.events[evt]) { let handler = Player.events[evt][selector]; if (typeof handler === 'string') { handler = Player[handler]; } const eventTarget = e.target.closest(selector); if (eventTarget) { e.eventTarget = eventTarget; return handler(e); } } }); } // Add audio event listeners. They don't bubble so do them separately. Player.audio.addEventListener('ended', Player.next); Player.audio.addEventListener('pause', () => Player.$(`.${ns}-video`).pause()); Player.audio.addEventListener('play', () => { Player.$(`.${ns}-video`).currentTime = Player.audio.currentTime; Player.$(`.${ns}-video`).play(); }); Player.audio.addEventListener('seeked', () => Player.$(`.${ns}-video`).currentTime = Player.audio.currentTime); } catch (err) { _logError('There was an error rendering the sound player. Please check the console for details.'); console.error('[4chan sounds player]'); // Can't recover, throw. throw err; } }, renderHeader: function () { Player.$(`.${ns}-title`).innerHTML = Player.templates.header(Player._tplOptions()); }, renderList: function () { if (Player.$(`.${ns}-list`)) { Player.$(`.${ns}-list`).innerHTML = Player.templates.list(Player._tplOptions()); } }, toggleDisplay: function (e) { e && e.preventDefault(); if (Player.container.style.display === 'none') { Player.show(); } else { Player.hide(); } }, hide: function (e) { try { e && e.preventDefault(); Player.container.style.display = 'none'; if (Player.settings.pauseOnHide) { Player.pause(); } } catch (err) { _logError('There was an error hiding the sound player. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, show: async function (e) { try { e && e.preventDefault(); if (!Player.container.style.display) { return; } Player.container.style.display = null; // Apply the last position/size const [ top, left ] = (await GM.getValue(ns + '.position') || '').split(':'); const [ width, height ] = (await GM.getValue(ns + '.size') || '').split(':'); +width && +height && Player.resizeTo(width, height); +top && +left && Player.moveTo(top, left); } catch (err) { _logError('There was an error showing the sound player. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, toggleRepeat: function (e) { try { e.preventDefault(); const options = Object.keys(headerOptions.repeat); const current = options.indexOf(Player.settings.repeat); Player.settings.repeat = options[(current + 4) % 3]; Player.renderHeader(); Player.saveSettings(); } catch (err) { _logError('There was an error changing the repeat setting. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, toggleShuffle: function (e) { try { e.preventDefault(); Player.settings.shuffle = !Player.settings.shuffle; Player.renderHeader(); // Update the play order. if (!Player.settings.shuffle) { Player.playOrder = [ ...Player.sounds ]; } else { const playOrder = Player.playOrder; for (let i = playOrder.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [playOrder[i], playOrder[j]] = [playOrder[j], playOrder[i]]; } } Player.saveSettings(); } catch (err) { _logError('There was an error changing the shuffle setting. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, toggleSettings: function (e) { try { e.preventDefault(); if (Player.container.classList.contains(ns + '-show-settings')) { Player.container.classList.remove(ns + '-show-settings'); } else { Player.container.classList.add(ns + '-show-settings'); } } catch (err) { _logError('There was an error rendering the sound player settings. Please check the console for details.'); console.error('[4chan sounds player]'); // Can't recover, throw. throw err; } }, handleSettingChange: function (e) { try { const input = e.eventTarget; const property = input.getAttribute('data-property'); const settingConfig = settingsConfig.find(settingConfig => settingConfig.property === property); const currentValue = _.get(Player.settings, property); let newValue = input[input.getAttribute('type') === 'checkbox' ? 'checked' : 'value']; if (settingConfig && settingConfig.split) { newValue = newValue.split(decodeURIComponent(settingConfig.split)); } // Not the most stringent check but enough to avoid some spamming. if (currentValue !== newValue) { // Update the setting. _.set(Player.settings, property, newValue); // Update the stylesheet reflect any changes. Player.stylesheet.innerHTML = Player.templates.css(Player._tplOptions()); // Save the new settings. Player.saveSettings(); } } catch (err) { _logError('There was an updating the setting. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, initResize: function initDrag(e) { disableUserSelect(); Player._startX = e.clientX; Player._startY = e.clientY; Player._startWidth = parseInt(document.defaultView.getComputedStyle(Player.container).width, 10); Player._startHeight = parseInt(document.defaultView.getComputedStyle(Player.container).height, 10); document.documentElement.addEventListener('mousemove', Player.doResize, false); document.documentElement.addEventListener('mouseup', Player.stopResize, false); }, doResize: function(e) { Player.resizeTo(Player._startWidth + e.clientX - Player._startX, Player._startHeight + e.clientY - Player._startY); }, resizeTo: function (width, height) { // Make sure the player isn't going off screen. 40 to give a bit of spacing for the 4chanX header. height = Math.min(height, document.documentElement.clientHeight - 40); Player.container.style.width = width + 'px'; // Change the height of the playlist of image. const heightElement = Player.settings.playlist ? Player.$(`.${ns}-list-container`) : Player.$(`.${ns}-image-link`); const containerHeight = parseInt(document.defaultView.getComputedStyle(Player.container).height, 10); const offset = containerHeight - parseInt(heightElement.style.height, 10); heightElement.style.height = Math.max(10, height - offset) + 'px'; }, stopResize: function() { const style = document.defaultView.getComputedStyle(Player.container); document.documentElement.removeEventListener('mousemove', Player.doResize, false); document.documentElement.removeEventListener('mouseup', Player.stopResize, false); enableUserSelect(); GM.setValue(ns + '.size', parseInt(style.width, 10) + ':' + parseInt(style.height, 10)); }, initMove: function (e) { disableUserSelect(); Player.$(`.${ns}-title`).style.cursor = 'grabbing'; // Try to reapply the current sizing to fix oversized winows. const style = document.defaultView.getComputedStyle(Player.container); Player.resizeTo(parseInt(style.width, 10), parseInt(style.height, 10)); Player._offsetX = e.clientX - Player.container.offsetLeft; Player._offsetY = e.clientY - Player.container.offsetTop; document.documentElement.addEventListener('mousemove', Player.doMove, false); document.documentElement.addEventListener('mouseup', Player.stopMove, false); }, doMove: function (e) { Player.moveTo(e.clientX - Player._offsetX, e.clientY - Player._offsetY); }, moveTo: function (x, y) { const style = document.defaultView.getComputedStyle(Player.container); const maxX = document.documentElement.clientWidth - parseInt(style.width, 10); const maxY = document.documentElement.clientHeight - parseInt(style.height, 10); Player.container.style.left = Math.max(0, Math.min(x, maxX)) + 'px'; Player.container.style.top = Math.max(0, Math.min(y, maxY)) + 'px'; }, stopMove: function (e) { document.documentElement.removeEventListener('mousemove', Player.doMove, false); document.documentElement.removeEventListener('mouseup', Player.stopMove, false); Player.$(`.${ns}-title`).style.cursor = null; enableUserSelect(); GM.setValue(ns + '.position', parseInt(Player.container.style.left, 10) + ':' + parseInt(Player.container.style.top, 10)); }, showThumb: function (sound) { try { Player.$(`.${ns}-image-link`).classList.remove(ns + '-show-video'); Player.$(`.${ns}-image`).src = sound.thumb; Player.$(`.${ns}-image-link`).href = sound.image; } catch (err) { _logError('There was an error displaying the sound player image. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, showImage: function (sound) { try { Player.$(`.${ns}-image-link`).classList.remove(ns + '-show-video'); Player.$(`.${ns}-image`).src = sound.image; Player.$(`.${ns}-image-link`).href = sound.image; } catch (err) { _logError('There was an error display the sound player image. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, playVideo: function (sound) { try { Player.$(`.${ns}-image-link`).classList.add(ns + '-show-video'); Player.$(`.${ns}-video`).src = sound.image; Player.$(`.${ns}-video`).play(); } catch (err) { _logError('There was an error display the sound player image. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, hidePlaylist: function () { try { Player.settings.playlist = false; Player.container.classList.add(`${ns}-expanded-view`); Player.container.classList.remove(`${ns}-playlist-view`); Player.saveSettings(); } catch (err) { _logError('There was an error switching to image view. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, showPlaylist: function () { try { Player.settings.playlist = true; Player.container.classList.remove(`${ns}-expanded-view`); Player.container.classList.add(`${ns}-playlist-view`); Player.saveSettings(); } catch (err) { _logError('There was an error switching to playlist view. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, togglePlaylist: function (e) { e && e.preventDefault(); if (Player.settings.playlist) { Player.hidePlaylist(); } else { Player.showPlaylist(); } }, add: function (title, id, src, thumb, image) { try { // Avoid duplicate additions. if (Player.sounds.find(sound => sound.id === id)) { return; } const sound = { title, src, id, thumb, image }; Player.sounds.push(sound); // Add the sound to the play order at the end, or someone random for shuffled. const index = Player.settings.shuffle ? Math.floor(Math.random() * Player.sounds.length - 1) : Player.sounds.length; Player.playOrder.splice(index, 0, sound); // Re-render the list. Player.renderList(); Player.$(`.${ns}-count`).innerHTML = Player.sounds.length; // If nothing else has been added yet show the image for this sound. if (Player.playOrder.length === 1) { // If we're on a thread with autoshow enabled then make sure the player is displayed if (/\/thread\//.test(location.href) && Player.settings.autoshow) { Player.show(); } Player.showThumb(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, thumg, image); console.error('[4chan sounds player]', err); } }, play: function (sound) { try { if (sound) { if (Player.playing) { Player.playing.playing = false; } sound.playing = true; Player.playing = sound; Player.renderHeader(); Player.audio.src = sound.src; if (sound.image.endsWith('.webm')) { Player.playVideo(sound); } else { Player.showImage(sound); } Player.renderList(); } Player.audio.play(); } catch (err) { _logError('There was an error playing the sound. Please check the console for details.'); console.error('[4chan sounds player]', err); } }, pause: function () { Player.audio.pause(); }, next: function () { Player._movePlaying(1); }, previous: function () { Player._movePlaying(-1); }, _movePlaying: function (direction) { try { // If there's no sound fall out. if (!Player.playOrder.length) { return; } // If there's no sound currently playing or it's not in the list then just play the first sound. const currentIndex = Player.playOrder.indexOf(Player.playing); if (currentIndex === -1) { return Player.playSound(Player.playOrder[0]); } // Get the next index, either repeating the same, wrapping round to repeat all or just moving the index. const nextIndex = Player.settings.repeat === 'one' ? currentIndex : Player.settings.repeat === 'all' ? ((currentIndex + direction) + Player.playOrder.length) % Player.playOrder.length : currentIndex + direction; const nextSound = Player.playOrder[nextIndex]; nextSound && Player.play(nextSound); } catch (err) { _logError(`There was an error selecting the ${direction > 0 ? 'next': 'previous'} track. Please check the console for details.`); console.error('[4chan sounds player]', err); } } }; _.templateSettings.imports.ns = ns; _.templateSettings.imports.Player = Player; _.templateSettings.imports.settingsConfig = settingsConfig; _.templateSettings.imports.headerOptions = headerOptions; document.addEventListener('DOMContentLoaded', async function() { await Player.initialize(); parseFiles(document.body); const observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(function (node) { if (node.nodeType === Node.ELEMENT_NODE) { parseFiles(node); } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); }); document.addEventListener('4chanXInitFinished', function () { isChanX = true; Player.initChanX(); }); function parseFiles (target) { target.querySelectorAll('.post').forEach(function (post) { if (post.parentElement.parentElement.id === 'qp' || post.parentElement.classList.contains('noFile')) { return; } post.querySelectorAll('.file').forEach(function (file) { parseFile(file, post); }); }); }; function parseFile(file, post) { try { if (!file.classList.contains('file')) { return; } const fileLink = isChanX ? file.querySelector('.fileText .file-info > a') : file.querySelector('.fileText > a'); if (!fileLink) { return; } if (!fileLink.href) { return; } let fileName = null; if (isChanX) { [ file.querySelector('.fileText .file-info .fnfull'), file.querySelector('.fileText .file-info > a') ].some(function (node) { return node && (fileName = node.textContent); }); } else { [ file.querySelector('.fileText'), file.querySelector('.fileText > a') ].some(function (node) { return node && (fileName = node.title || node.tagName === 'A' && node.textContent); }); } if (!fileName) { return; } fileName = fileName.replace(/\-/, '/'); const match = fileName.match(/^(.*)[\[\(\{](?:audio|sound)[ \=\:\|\$](.*?)[\]\)\}]/i); if (!match) { return; } const id = post.id.slice(1); const name = match[1] || id; const fileThumb = post.querySelector('.fileThumb'); const fullSrc = fileThumb && fileThumb.href; const thumbSrc = fileThumb && fileThumb.querySelector('img').src; let link = match[2]; if (link.includes('%')) { try { link = decodeURIComponent(link); } catch (error) { return; } } if (link.match(/^(https?\:)?\/\//) === null) { link = (location.protocol + '//' + link); } try { link = new URL(link); } catch (error) { return; } for (let item of Player.settings.allow) { if (link.hostname.toLowerCase() === item || link.hostname.toLowerCase().endsWith('.' + item)) { return Player.add(name, id, link.href, thumbSrc, fullSrc); } } } catch (err) { _logError('There was an issue parsing the files. Please check the console for details.'); console.log('[4chan sounds player]', post) console.error(err); } }; function disableUserSelect () { document.body.style.userSelect = 'none'; document.body.style.MozUserSelect = 'none'; } function enableUserSelect () { document.body.style.userSelect = null; document.body.style.MozUserSelect = null; } })();