// ==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
// ==/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<n;)o[r]=e(t[r],r,t);return o}function r(t){return function(e){return t(e)}}function n(t,r){return e(r,function(e){return t[e]})}function o(t){return"\\"+ft[t]}function u(t,e){return function(r){return t(e(r))}}function c(){}function i(t){var e=-1,r=null==t?0:t.length;
for(this.clear();++e<r;){var n=t[e];this.set(n[0],n[1])}}function a(t){var e=-1,r=null==t?0:t.length;for(this.clear();++e<r;){var n=t[e];this.set(n[0],n[1])}}function l(t){var e=-1,r=null==t?0:t.length;for(this.clear();++e<r;){var n=t[e];this.set(n[0],n[1])}}function f(t,e){var r=Ht(t),n=!r&&Gt(t),o=!r&&!n&&Jt(t),u=!r&&!n&&!o&&Kt(t);if(r=r||n||o||u){for(var n=t.length,c=String,i=-1,a=Array(n);++i<n;)a[i]=c(i);n=a}else n=[];var l,c=n.length;for(l in t)!e&&!St.call(t,l)||r&&("length"==l||o&&("offset"==l||"parent"==l)||u&&("buffer"==l||"byteLength"==l||"byteOffset"==l)||A(l,c))||n.push(l);
return n}function s(t,e,r){var n=t[e];St.call(t,e)&&F(n,r)&&(r!==V||e in t)||_(t,e,r)}function p(t,e){for(var r=t.length;r--;)if(F(t[r][0],e))return r;return-1}function _(t,e,r){"__proto__"==e&&Lt?Lt(t,e,{configurable:true,enumerable:true,value:r,writable:true}):t[e]=r}function h(t){if(null==t)return t===V?"[object Undefined]":"[object Null]";if(Ut&&Ut in Object(t)){var e=St.call(t,Ut),r=t[Ut];try{t[Ut]=V;var n=true}catch(t){}var o=xt.call(t);n&&(e?t[Ut]=r:delete t[Ut]),t=o}else t=xt.call(t);return t}function b(t){
return P(t)&&"[object Arguments]"==h(t)}function y(t){return P(t)&&U(t.length)&&!!at[h(t)]}function g(t,e){return qt(x(t,e,W),t+"")}function j(t){if(typeof t=="string")return t;if(Ht(t))return e(t,j)+"";if(B(t))return Wt?Wt.call(t):"";var r=t+"";return"0"==r&&1/t==-G?"-0":r}function v(t,e){if(Ht(t))return t;var r;return Ht(t)?r=false:(r=typeof t,r=!("number"!=r&&"symbol"!=r&&"boolean"!=r&&null!=t&&!B(t))||(tt.test(t)||!Z.test(t)||null!=e&&t in Object(e))),r?[t]:Vt(C(t))}function d(t,e,r,n){return t===V||F(t,mt[r])&&!St.call(n,r)?e:t;
}function m(t,e){var r=t.__data__,n=typeof e;return("string"==n||"number"==n||"symbol"==n||"boolean"==n?"__proto__"!==e:null===e)?r[typeof e=="string"?"string":"hash"]:r.map}function O(t,e){var r,n=null==t?V:t[e];return r=!(!L(n)||wt&&wt in n)&&(I(n)?Et:ot).test(E(n)),r?n:V}function A(t,e){var r=typeof t;return e=null==e?9007199254740991:e,!!e&&("number"==r||"symbol"!=r&&ut.test(t))&&-1<t&&0==t%1&&t<e}function S(t,e,r){if(!L(r))return false;var n=typeof e;return!!("number"==n?k(r)&&A(e,r.length):"string"==n&&e in r)&&F(r[e],t);
}function w(t){var e=t&&t.constructor;return t===(typeof e=="function"&&e.prototype||mt)}function x(e,r,n){return r=Bt(r===V?e.length-1:r,0),function(){for(var o=arguments,u=-1,c=Bt(o.length-r,0),i=Array(c);++u<c;)i[u]=o[r+u];for(u=-1,c=Array(r+1);++u<r;)c[u]=o[u];return c[r]=n(i),t(e,this,c)}}function $(t){if(typeof t=="string"||B(t))return t;var e=t+"";return"0"==e&&1/t==-G?"-0":e}function E(t){if(null!=t){try{return At.call(t)}catch(t){}return t+""}return""}function z(t,e){if(typeof t!="function"||null!=e&&typeof e!="function")throw new TypeError("Expected a function");
var r=function(){var n=arguments,o=e?e.apply(this,n):n[0],u=r.cache;return u.has(o)?u.get(o):(n=t.apply(this,n),r.cache=u.set(o,n)||u,n)};return r.cache=new(z.Cache||l),r}function F(t,e){return t===e||t!==t&&e!==e}function k(t){return null!=t&&U(t.length)&&!I(t)}function R(t){if(!P(t))return false;var e=h(t);return"[object Error]"==e||"[object DOMException]"==e||typeof t.message=="string"&&typeof t.name=="string"&&!T(t)}function I(t){return!!L(t)&&(t=h(t),"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t);
}function U(t){return typeof t=="number"&&-1<t&&0==t%1&&9007199254740991>=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-1<p(this.__data__,t)},a.prototype.set=function(t,e){var r=this.__data__,n=p(r,t);return 0>n?(++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,0<o){if(800<=++e)return arguments[0]}else e=0;return t.apply(V,arguments)}}(Lt?function(t,e){return Lt(t,"toString",{configurable:true,enumerable:false,value:N(e),writable:true})}:W),Vt=function(t){t=z(t,function(t){return 500===e.size&&e.clear(),t});var e=t.cache;return t}(function(t){var e=[];return 46===t.charCodeAt(0)&&e.push(""),
t.replace(et,function(t,r,n,o){e.push(n?o.replace(rt,"$1"):r||t)}),e});z.Cache=l;var Gt=b(function(){return arguments}())?b:function(t){return P(t)&&St.call(t,"callee")&&!Rt.call(t,"callee")},Ht=Array.isArray,Jt=Pt||q,Kt=jt?r(jt):y,Qt=function(t){return g(function(e,r){var n=-1,o=r.length,u=1<o?r[o-1]:V,c=2<o?r[2]:V,u=3<t.length&&typeof u=="function"?(o--,u):V;for(c&&S(r[0],r[1],c)&&(u=3>o?V:u,o=1),e=Object(e);++n<o;)(c=r[n])&&t(e,c,n,u);return e})}(function(t,e,r,n){r=D(e);var o=!t;t||(t={});for(var u=-1,c=r.length;++u<c;){
var i=r[u],a=n?n(t[i],e[i],i,t,e):V;a===V&&(a=e[i]),o?_(t,i,a):s(t,i,a)}}),Xt=g(function(e,r){try{return t(e,V,r)}catch(t){return R(t)?t:Error(t)}});c.assignInWith=Qt,c.constant=N,c.keys=M,c.keysIn=D,c.memoize=z,c.set=function(t,e,r){if(null!=t&&L(t)){e=v(e,t);for(var n=-1,o=e.length,u=o-1,c=t;null!=c&&++n<o;){var i=$(e[n]),a=r;if(n!=u){var l=c[i],a=V;a===V&&(a=L(l)?l:A(e[n+1])?[]:{})}s(c,i,a),c=c[i]}}return t},c.extendWith=Qt,c.attempt=Xt,c.eq=F,c.escape=function(t){return(t=C(t))&&X.test(t)?t.replace(Q,vt):t;
},c.get=function(t,e,r){if(null==t)t=V;else{e=v(e,t);for(var n=0,o=e.length;null!=t&&n<o;)t=t[$(e[n++])];t=n&&n==o?t:V}return t===V?r:t},c.identity=W,c.isArguments=Gt,c.isArray=Ht,c.isArrayLike=k,c.isBuffer=Jt,c.isError=R,c.isFunction=I,c.isLength=U,c.isObject=L,c.isObjectLike=P,c.isPlainObject=T,c.isSymbol=B,c.isTypedArray=Kt,c.stubFalse=q,c.template=function(t,e,r){var u=c.templateSettings;r&&S(t,e,r)&&(e=V),t=C(t),e=Qt({},e,u,d),r=Qt({},e.imports,u.imports,d);var i,a,l=M(r),f=n(r,l),s=0;r=e.interpolate||ct;
var p="__p+='",_="sourceURL"in e?"//# sourceURL="+e.sourceURL+"\n":"";if(t.replace(RegExp((e.escape||ct).source+"|"+r.source+"|"+(r===Y?nt:ct).source+"|"+(e.evaluate||ct).source+"|$","g"),function(e,r,n,u,c,l){return n||(n=u),p+=t.slice(s,l).replace(it,o),r&&(i=true,p+="'+__e("+r+")+'"),c&&(a=true,p+="';"+c+";\n__p+='"),n&&(p+="'+((__t=("+n+"))==null?'':__t)+'"),s=l+e.length,e}),p+="';",(e=e.variable)||(p="with(obj){"+p+"}"),p=(a?p.replace(H,""):p).replace(J,"$1").replace(K,"$1;"),p="function("+(e||"obj")+"){"+(e?"":"obj||(obj={});")+"var __t,__p=''"+(i?",__e=_.escape":"")+(a?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+p+"return __p}",
e=Xt(function(){return Function(l,_+"return "+p).apply(V,f)}),e.source=p,R(e))throw e;return e},c.toString=C,c.VERSION="4.17.5",typeof define=="function"&&typeof define.amd=="object"&&define.amd?(_t._=c, define(function(){return c})):bt?((bt.exports=c)._=c,ht._=c):_t._=c}).call(this);
(function() {
'use strict';
let isChanX;
const _ = self._;
const ns = 'fc-sounds';
function _logError(message, type = 'error') {
document.dispatchEvent(new CustomEvent("CreateNotification", {
bubbles: true,
detail: {
type: type,
content: message,
lifetime: 5
}
}));
}
const settingsConfig = [
{
property: 'shuffle',
default: false
},
{
property: 'repeat',
default: 'all'
},
{
property: 'playlist',
default: true
},
{
property: 'autoshow',
default: true,
title: 'Autoshow',
description: 'Automatically show the player when the thread contains sounds.',
showInSettings: true
},
{
property: 'pauseOnHide',
default: true,
title: 'Pause on hide',
description: 'Pause the player when it\'s hidden.',
showInSettings: true
},
{
property: 'allow',
default: [
'4cdn.org',
'catbox.moe',
'dmca.gripe',
'lewd.se',
'pomf.cat',
'zz.ht'
],
title: 'Allow',
description: 'Which domains sources are allowed to be loaded from.',
showInSettings: true,
split: '\n'
},
{
property: 'colors.background',
default: '#d6daf0',
title: 'Background Color',
showInSettings: true
},
{
property: 'colors.border',
default: '#b7c5d9',
title: 'Border Color',
showInSettings: true
},
{
property: 'colors.odd_row',
default: '#d6daf0',
title: 'Odd Row Color',
showInSettings: true
},
{
property: 'colors.even_row',
default: '#b7c5d9',
title: 'Even Row Color',
showInSettings: true
},
{
property: 'colors.playing',
default: '#98bff7',
title: 'Playing Row Color',
showInSettings: true
},
{
property: 'colors.expander',
default: '#808bbf',
title: 'Expander Color',
showInSettings: true
},
{
property: 'colors.expander_hover',
default: '#9aa6e1',
title: 'Expander Hover Color',
showInSettings: true
}
]
const headerOptions = {
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: 'Shuffle', text: '[S]', class: 'fa-random' },
false: { title: 'Ordered', text: '[S]', class: 'fa-random disabled' },
},
playlist: {
true: { title: 'Hide Playlist', text: '[+]', class: 'fa-expand' },
false: { title: 'Show Playlist', text: '[+]', class: 'fa-compress' }
}
}
const Player = {
ns,
sounds: [],
container: null,
ui: {},
settings: settingsConfig.reduce((settings, settingConfig) => {
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: "<div id=\"<%= ns %>-container\" class=\"<%= ns %>-<%= data.playlist ? 'playlist' : 'extended' %>-view\" style=\"top: 100px; left: 100px; width: 350px; display: none;\">\n\t<div class=\"<%= ns %>-title\" style=\"display: flex; flex-wrap: wrap; justify-content: between;\">\n\t\t<%= Player.templates.header({ data }) %>\n\t</div>\n\t<div class=\"<%= ns %>-player\">\n\t\t<a class=\"<%= ns %>-image-link\" style=\"height: 128px\" target=\"_blank\">\n\t\t\t<img class=\"<%= ns %>-image\"></img>\n\t\t\t<video class=\"<%= ns %>-video\"></video>\n\t\t</a>\n\t\t<div class=\"<%= ns %>-controls\">\n\t\t\t<div class=\"<%= ns %>-play-pause <%=ns %>-<%= !Player.audio || Player.audio.paused ? 'play' : 'pause' %>>\"></div>\n\t\t\t<div class=\"<%= ns %>-seekbar\"></div>\n\t\t\t<div class=\"<%= ns %>-duration\"></div>\n\t\t\t<div class=\"<%= ns %>-volume\"></div>\n\t\t</div>\n\t\t<audio class=\"<%= ns %>-audio\" controls=\"true\"></audio>\n\t\t<div class=\"<%= ns %>-list-container\" style=\"height: 100px\">\n\t\t\t<ul class=\"<%= ns %>-list\">\n\t\t\t\t<%= Player.templates.list({ data }) %>\n\t\t\t</ul>\n\t\t</div>\n\t\t<div class=\"<%= ns %>-footer\">\n\t\t\t<span class=\"<%= ns %>-count\">0</span> sounds\n\t\t\t<div class=\"<%= ns %>-expander\"></div>\n\t\t</div>\n\t</div>\n\t<div class=\"<%= ns %>-settings\">\n\t\t<%= Player.templates.settings({ data }) %>\n\t</div>\n</div>",
header: "<div style=\"flex: 0 0 auto; width: auto; max-width: 100%; margin-left: 0.25rem;\">\n\t<% Object.keys(headerOptions).forEach(key => { %>\n\t\t<% let option = headerOptions[key][data[key]] || headerOptions[key][0]; %>\n\t\t<a class=\"<%= ns %>-<%= key %>-button fa <%= option.class %>\" title=\"<%= option.title %>\" href=\"javascript;\">\n\t\t\t<%= option.text %>\n\t\t</a>\n\t<% }) %>\n</div><div style=\"flex-basis: 0; flex-grow: 1; max-width: 100%; width: 100%; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;\">\n\t<%= Player.playing ? Player.playing.title : '4chan Sounds' %>\n</div>\n<div style=\"flex: 0 0 auto; width: auto; max-width: 100%; margin-right: 0.25rem;\">\n\t<a class=\"<%= ns %>-config-button fa fa-wrench\" title=\"Settings\" href=\"javascript;\">Settings</a>\n\t<a class=\"<%= ns %>-close-button fa fa-times\" href=\"javascript;\">X</a>\n</div>",
list: "<% Player.sounds.forEach(sound => { %>\n\t<li class=\"<%= ns %>-list-item <%= sound.playing ? 'playing' : '' %>\" data-id=\"<%= sound.id %>\">\n\t\t<%= sound.title %>\n\t</li>\n<% }) %>",
settings: "<% settingsConfig.forEach(setting => { %>\n\t<% if (setting.showInSettings) { %>\n\t\t<div class=\"<%= ns %>-setting-header\" <%= setting.description ? `title=\"${setting.description}\"` : '' %>><%= setting.title %></div>\n\t\t<% if (typeof setting.default === 'boolean') { %>\n\t\t\t<input type=\"checkbox\" data-property=\"<%= setting.property %>\" <%= _.get(data, setting.property, false) ? 'checked' : '' %>></input>\n\t\t<% } else if (Array.isArray(setting.default)) { %>\n\t\t\t<textarea data-property=\"<%= setting.property %>\"><%= _.get(data, setting.property, '').join(setting.split) %></textarea>\n\t\t<% } else { %>\n\t\t\t<input type=\"text\" data-property=\"<%= setting.property %>\" value=\"<%= _.get(data, setting.property, '') %>\"></input>\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 = '<a href="javascript:;" title="Sounds" class="fa fa-play-circle">Sounds</a>';
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;
}
})();