// ==UserScript==
// @name 4chan sounds player
// @version 3.2.1
// @namespace rccom
// @description A player designed for 4chan sounds threads.
// @author RCC
// @match *://boards.4chan.org/*
// @match *://boards.4channel.org/*
// @match *://desuarchive.org/*
// @match *://arch.b4k.co/*
// @match *://archived.moe/*
// @match *://warosu.org/*
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.xmlHttpRequest
// @grant GM_addValueChangeListener
// @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 pomf.cat
// @connect files.catbox.moe
// @connect catbox.moe
// @connect share.dmca.gripe
// @connect z.zz.ht
// @connect zz.ht
// @connect too.lewd.se
// @connect lewd.se
// @connect *
// @run-at document-start
// ==/UserScript==
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/main.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./node_modules/bootstrap-icons/icons/arrow-clockwise.svg":
/*!****************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/arrow-clockwise.svg ***!
\****************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-arrow-clockwise\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z\"/>\n <path d=\"M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/arrow-repeat.svg":
/*!*************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/arrow-repeat.svg ***!
\*************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-arrow-repeat\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z\"/>\n <path fill-rule=\"evenodd\" d=\"M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/arrows-collapse.svg":
/*!****************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/arrows-collapse.svg ***!
\****************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-arrows-collapse\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M1 8a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13A.5.5 0 0 1 1 8zm7-8a.5.5 0 0 1 .5.5v3.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 1 1 .708-.708L7.5 4.293V.5A.5.5 0 0 1 8 0zm-.5 11.707l-1.146 1.147a.5.5 0 0 1-.708-.708l2-2a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 11.707V15.5a.5.5 0 0 1-1 0v-3.793z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/arrows-expand.svg":
/*!**************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/arrows-expand.svg ***!
\**************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-arrows-expand\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M1 8a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13A.5.5 0 0 1 1 8zM7.646.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 1.707V5.5a.5.5 0 0 1-1 0V1.707L6.354 2.854a.5.5 0 1 1-.708-.708l2-2zM8 10a.5.5 0 0 1 .5.5v3.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 0 1 .708-.708L7.5 14.293V10.5A.5.5 0 0 1 8 10z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/bootstrap-reboot.svg":
/*!*****************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/bootstrap-reboot.svg ***!
\*****************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-bootstrap-reboot\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M1.161 8a6.84 6.84 0 1 0 6.842-6.84.58.58 0 0 1 0-1.16 8 8 0 1 1-6.556 3.412l-.663-.577a.58.58 0 0 1 .227-.997l2.52-.69a.58.58 0 0 1 .728.633l-.332 2.592a.58.58 0 0 1-.956.364l-.643-.56A6.812 6.812 0 0 0 1.16 8zm5.48-.079V5.277h1.57c.881 0 1.416.499 1.416 1.32 0 .84-.504 1.324-1.386 1.324h-1.6zm0 3.75V8.843h1.57l1.498 2.828h1.314L9.377 8.665c.897-.3 1.427-1.106 1.427-2.1 0-1.37-.943-2.246-2.456-2.246H5.5v7.352h1.141z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/chat-right-quote.svg":
/*!*****************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/chat-right-quote.svg ***!
\*****************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-chat-right-quote\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M2 1h12a1 1 0 0 1 1 1v11.586l-2-2A2 2 0 0 0 11.586 11H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm12-1a2 2 0 0 1 2 2v12.793a.5.5 0 0 1-.854.353l-2.853-2.853a1 1 0 0 0-.707-.293H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h12z\"/>\n <path fill-rule=\"evenodd\" d=\"M7.066 4.76A1.665 1.665 0 0 0 4 5.668a1.667 1.667 0 0 0 2.561 1.406c-.131.389-.375.804-.777 1.22a.417.417 0 1 0 .6.58c1.486-1.54 1.293-3.214.682-4.112zm4 0A1.665 1.665 0 0 0 8 5.668a1.667 1.667 0 0 0 2.561 1.406c-.131.389-.375.804-.777 1.22a.417.417 0 1 0 .6.58c1.486-1.54 1.293-3.214.682-4.112z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/chevron-down.svg":
/*!*************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/chevron-down.svg ***!
\*************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-chevron-down\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/file-earmark-image.svg":
/*!*******************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/file-earmark-image.svg ***!
\*******************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-file-earmark-image\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M12 16a2 2 0 0 0 2-2V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8zM3 2a1 1 0 0 1 1-1h5.5v2A1.5 1.5 0 0 0 11 4.5h2V10l-2.083-2.083a.5.5 0 0 0-.76.063L8 11 5.835 9.7a.5.5 0 0 0-.611.076L3 12V2z\"/>\n <path fill-rule=\"evenodd\" d=\"M6.502 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/file-earmark-music.svg":
/*!*******************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/file-earmark-music.svg ***!
\*******************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-file-earmark-music\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M4 0h5.5v1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h1V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z\"/>\n <path d=\"M9.5 3V0L14 4.5h-3A1.5 1.5 0 0 1 9.5 3z\"/>\n <path fill-rule=\"evenodd\" d=\"M9.757 5.67A1 1 0 0 1 11 6.64v1.75l-2 .5v3.61c0 .495-.301.883-.662 1.123C7.974 13.866 7.499 14 7 14c-.5 0-.974-.134-1.338-.377-.36-.24-.662-.628-.662-1.123s.301-.883.662-1.123C6.026 11.134 6.501 11 7 11c.356 0 .7.068 1 .196V6.89a1 1 0 0 1 .757-.97l1-.25z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/filter.svg":
/*!*******************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/filter.svg ***!
\*******************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-filter\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/fullscreen-exit.svg":
/*!****************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/fullscreen-exit.svg ***!
\****************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-fullscreen-exit\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5zm5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5zM0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zm10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/fullscreen.svg":
/*!***********************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/fullscreen.svg ***!
\***********************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-fullscreen\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M1.5 1a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4A1.5 1.5 0 0 1 1.5 0h4a.5.5 0 0 1 0 1h-4zM10 .5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 16 1.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zM.5 10a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 0 14.5v-4a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 1-1.5 1.5h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/gear.svg":
/*!*****************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/gear.svg ***!
\*****************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-gear\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M8.837 1.626c-.246-.835-1.428-.835-1.674 0l-.094.319A1.873 1.873 0 0 1 4.377 3.06l-.292-.16c-.764-.415-1.6.42-1.184 1.185l.159.292a1.873 1.873 0 0 1-1.115 2.692l-.319.094c-.835.246-.835 1.428 0 1.674l.319.094a1.873 1.873 0 0 1 1.115 2.693l-.16.291c-.415.764.42 1.6 1.185 1.184l.292-.159a1.873 1.873 0 0 1 2.692 1.116l.094.318c.246.835 1.428.835 1.674 0l.094-.319a1.873 1.873 0 0 1 2.693-1.115l.291.16c.764.415 1.6-.42 1.184-1.185l-.159-.291a1.873 1.873 0 0 1 1.116-2.693l.318-.094c.835-.246.835-1.428 0-1.674l-.319-.094a1.873 1.873 0 0 1-1.115-2.692l.16-.292c.415-.764-.42-1.6-1.185-1.184l-.291.159A1.873 1.873 0 0 1 8.93 1.945l-.094-.319zm-2.633-.283c.527-1.79 3.065-1.79 3.592 0l.094.319a.873.873 0 0 0 1.255.52l.292-.16c1.64-.892 3.434.901 2.54 2.541l-.159.292a.873.873 0 0 0 .52 1.255l.319.094c1.79.527 1.79 3.065 0 3.592l-.319.094a.873.873 0 0 0-.52 1.255l.16.292c.893 1.64-.902 3.434-2.541 2.54l-.292-.159a.873.873 0 0 0-1.255.52l-.094.319c-.527 1.79-3.065 1.79-3.592 0l-.094-.319a.873.873 0 0 0-1.255-.52l-.292.16c-1.64.893-3.433-.902-2.54-2.541l.159-.292a.873.873 0 0 0-.52-1.255l-.319-.094c-1.79-.527-1.79-3.065 0-3.592l.319-.094a.873.873 0 0 0 .52-1.255l-.16-.292c-.892-1.64.902-3.433 2.541-2.54l.292.159a.873.873 0 0 0 1.255-.52l.094-.319z\"/>\n <path fill-rule=\"evenodd\" d=\"M8 5.754a2.246 2.246 0 1 0 0 4.492 2.246 2.246 0 0 0 0-4.492zM4.754 8a3.246 3.246 0 1 1 6.492 0 3.246 3.246 0 0 1-6.492 0z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/image.svg":
/*!******************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/image.svg ***!
\******************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1.0625em\" height=\"1em\" viewBox=\"0 0 17 16\" class=\"bi bi-image\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M14.002 2h-12a1 1 0 0 0-1 1v9l2.646-2.354a.5.5 0 0 1 .63-.062l2.66 1.773 3.71-3.71a.5.5 0 0 1 .577-.094L15.002 9.5V3a1 1 0 0 0-1-1zm-12-1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2h-12zm4 4.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/link-45deg.svg":
/*!***********************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/link-45deg.svg ***!
\***********************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-link-45deg\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M4.715 6.542L3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.001 1.001 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z\"/>\n <path d=\"M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 0 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 0 0-4.243-4.243L6.586 4.672z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/music-note-list.svg":
/*!****************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/music-note-list.svg ***!
\****************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-music-note-list\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M12 13c0 1.105-1.12 2-2.5 2S7 14.105 7 13s1.12-2 2.5-2 2.5.895 2.5 2z\"/>\n <path fill-rule=\"evenodd\" d=\"M12 3v10h-1V3h1z\"/>\n <path d=\"M11 2.82a1 1 0 0 1 .804-.98l3-.6A1 1 0 0 1 16 2.22V4l-5 1V2.82z\"/>\n <path fill-rule=\"evenodd\" d=\"M0 11.5a.5.5 0 0 1 .5-.5H4a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5zm0-4A.5.5 0 0 1 .5 7H8a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5zm0-4A.5.5 0 0 1 .5 3H8a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/pause-fill.svg":
/*!***********************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/pause-fill.svg ***!
\***********************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-pause-fill\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/pause.svg":
/*!******************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/pause.svg ***!
\******************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-pause\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M6 3.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V4a.5.5 0 0 1 .5-.5zm4 0a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V4a.5.5 0 0 1 .5-.5z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/play-fill.svg":
/*!**********************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/play-fill.svg ***!
\**********************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-play-fill\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M11.596 8.697l-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/play.svg":
/*!*****************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/play.svg ***!
\*****************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-play\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M10.804 8L5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/plus-circle.svg":
/*!************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/plus-circle.svg ***!
\************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-plus-circle\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z\"/>\n <path fill-rule=\"evenodd\" d=\"M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/plus.svg":
/*!*****************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/plus.svg ***!
\*****************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-plus\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/search.svg":
/*!*******************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/search.svg ***!
\*******************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-search\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z\"/>\n <path fill-rule=\"evenodd\" d=\"M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/shuffle.svg":
/*!********************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/shuffle.svg ***!
\********************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-shuffle\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M0 3.5A.5.5 0 0 1 .5 3H1c2.202 0 3.827 1.24 4.874 2.418.49.552.865 1.102 1.126 1.532.26-.43.636-.98 1.126-1.532C9.173 4.24 10.798 3 13 3v1c-1.798 0-3.173 1.01-4.126 2.082A9.624 9.624 0 0 0 7.556 8a9.624 9.624 0 0 0 1.317 1.918C9.828 10.99 11.204 12 13 12v1c-2.202 0-3.827-1.24-4.874-2.418A10.595 10.595 0 0 1 7 9.05c-.26.43-.636.98-1.126 1.532C4.827 11.76 3.202 13 1 13H.5a.5.5 0 0 1 0-1H1c1.798 0 3.173-1.01 4.126-2.082A9.624 9.624 0 0 0 6.444 8a9.624 9.624 0 0 0-1.317-1.918C4.172 5.01 2.796 4 1 4H.5a.5.5 0 0 1-.5-.5z\"/>\n <path d=\"M13 5.466V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192zm0 9v-3.932a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/skip-end-fill.svg":
/*!**************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/skip-end-fill.svg ***!
\**************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-skip-end-fill\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M12 3.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V4a.5.5 0 0 1 .5-.5z\"/>\n <path d=\"M11.596 8.697l-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/skip-end.svg":
/*!*********************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/skip-end.svg ***!
\*********************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-skip-end\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M12 3.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V4a.5.5 0 0 1 .5-.5z\"/>\n <path fill-rule=\"evenodd\" d=\"M10.804 8L5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/skip-start-fill.svg":
/*!****************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/skip-start-fill.svg ***!
\****************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-skip-start-fill\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M4.5 3.5A.5.5 0 0 0 4 4v8a.5.5 0 0 0 1 0V4a.5.5 0 0 0-.5-.5z\"/>\n <path d=\"M4.903 8.697l6.364 3.692c.54.313 1.232-.066 1.232-.697V4.308c0-.63-.692-1.01-1.232-.696L4.903 7.304a.802.802 0 0 0 0 1.393z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/skip-start.svg":
/*!***********************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/skip-start.svg ***!
\***********************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-skip-start\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M4.5 3.5A.5.5 0 0 0 4 4v8a.5.5 0 0 0 1 0V4a.5.5 0 0 0-.5-.5z\"/>\n <path fill-rule=\"evenodd\" d=\"M5.696 8L11.5 4.633v6.734L5.696 8zm-.792-.696a.802.802 0 0 0 0 1.392l6.363 3.692c.52.302 1.233-.043 1.233-.696V4.308c0-.653-.713-.998-1.233-.696L4.904 7.304z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/soundwave.svg":
/*!**********************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/soundwave.svg ***!
\**********************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-soundwave\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M8.5 2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-1 0v-11a.5.5 0 0 1 .5-.5zm-2 2a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5zm4 0a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5zm-6 1.5A.5.5 0 0 1 5 6v4a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm8 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm-10 1A.5.5 0 0 1 3 7v2a.5.5 0 0 1-1 0V7a.5.5 0 0 1 .5-.5zm12 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0V7a.5.5 0 0 1 .5-.5z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/tools.svg":
/*!******************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/tools.svg ***!
\******************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-tools\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M0 1l1-1 3.081 2.2a1 1 0 0 1 .419.815v.07a1 1 0 0 0 .293.708L10.5 9.5l.914-.305a1 1 0 0 1 1.023.242l3.356 3.356a1 1 0 0 1 0 1.414l-1.586 1.586a1 1 0 0 1-1.414 0l-3.356-3.356a1 1 0 0 1-.242-1.023L9.5 10.5 3.793 4.793a1 1 0 0 0-.707-.293h-.071a1 1 0 0 1-.814-.419L0 1zm11.354 9.646a.5.5 0 0 0-.708.708l3 3a.5.5 0 0 0 .708-.708l-3-3z\"/>\n <path fill-rule=\"evenodd\" d=\"M15.898 2.223a3.003 3.003 0 0 1-3.679 3.674L5.878 12.15a3 3 0 1 1-2.027-2.027l6.252-6.341A3 3 0 0 1 13.778.1l-2.142 2.142L12 4l1.757.364 2.141-2.141zm-13.37 9.019L3.001 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/trash.svg":
/*!******************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/trash.svg ***!
\******************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-trash\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z\"/>\n <path fill-rule=\"evenodd\" d=\"M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/volume-mute-fill.svg":
/*!*****************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/volume-mute-fill.svg ***!
\*****************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-volume-mute-fill\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zm7.137 2.096a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708l4-4a.5.5 0 0 1 .708 0z\"/>\n <path fill-rule=\"evenodd\" d=\"M9.146 5.646a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/volume-mute.svg":
/*!************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/volume-mute.svg ***!
\************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-volume-mute\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04zm7.854.606a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708l4-4a.5.5 0 0 1 .708 0z\"/>\n <path fill-rule=\"evenodd\" d=\"M9.146 5.646a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/volume-up-fill.svg":
/*!***************************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/volume-up-fill.svg ***!
\***************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-volume-up-fill\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z\"/>\n <path d=\"M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z\"/>\n <path d=\"M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z\"/>\n <path fill-rule=\"evenodd\" d=\"M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06z\"/>\n</svg>");
/***/ }),
/***/ "./node_modules/bootstrap-icons/icons/volume-up.svg":
/*!**********************************************************!*\
!*** ./node_modules/bootstrap-icons/icons/volume-up.svg ***!
\**********************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ("<svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" class=\"bi bi-volume-up\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04L4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96V5.04z\"/>\n <path d=\"M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z\"/>\n <path d=\"M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z\"/>\n <path d=\"M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707z\"/>\n</svg>");
/***/ }),
/***/ "./src/_.js":
/*!******************!*\
!*** ./src/_.js ***!
\******************/
/*! no static exports found */
/***/ (function(module, exports) {
const _ = module.exports;
module.exports.set = function set(object, path, value) {
const props = path.split('.');
const lastProp = props.pop();
const setOn = props.reduce((obj, k) => obj[k] || (obj[k] = {}), object);
setOn && (setOn[lastProp] = value);
return object;
};
module.exports.get = function get(object, path, dflt) {
if (typeof path !== 'string') {
return dflt;
}
const props = path.split('.');
const lastProp = props.pop();
const parent = props.reduce((obj, k) => obj && obj[k], object);
return parent && Object.prototype.hasOwnProperty.call(parent, lastProp)
? parent[lastProp]
: dflt;
};
/**
* Check two values are equal. Arrays/Objects are deep checked.
*/
module.exports.isEqual = function isEqual(a, b, strict = true) {
if (typeof a !== typeof b) {
return false;
}
if (Array.isArray(a, b)) {
return a === b || a.length === b.length && a.every((_a, i) => isEqual(_a, b[i]));
}
if (a && b && typeof a === 'object' && a !== b) {
const allKeys = Object.keys(a);
allKeys.push(...Object.keys(b).filter(k => !allKeys.includes(k)));
return allKeys.every(key => _.isEqual(a[key], b[key]));
}
// eslint-disable-next-line eqeqeq
return strict ? a === b : a == b;
};
module.exports.toDuration = function toDuration(number) {
number = Math.floor(number || 0);
let [ seconds, minutes, hours ] = _duration(0, number);
seconds < 10 && (seconds = '0' + seconds);
return (hours ? hours + ':' : '') + minutes + ':' + seconds;
};
module.exports.timeAgo = function timeAgo(date) {
const [ seconds, minutes, hours, days, weeks ] = _duration(Math.floor(date), Math.floor(Date.now() / 1000));
/* _eslint-disable indent */
return weeks > 1 ? weeks + ' weeks ago'
: days > 0 ? days + (days === 1 ? ' day' : ' days') + ' ago'
: hours > 0 ? hours + (hours === 1 ? ' hour' : ' hours') + ' ago'
: minutes > 0 ? minutes + (minutes === 1 ? ' minute' : ' minutes') + ' ago'
: seconds + (seconds === 1 ? ' second' : ' seconds') + ' ago';
/* eslint-enable indent */
};
function _duration(from, to) {
const diff = Math.max(0, to - from);
return [
diff % 60,
Math.floor(diff / 60) % 60,
Math.floor(diff / 60 / 60) % 24,
Math.floor(diff / 60 / 60 / 24) % 7,
Math.floor(diff / 60 / 60 / 24 / 7)
];
}
module.exports.element = function element(html, parent, events = {}) {
const container = document.createElement('div');
container.innerHTML = html;
const el = container.children[0];
parent && parent.appendChild(el);
for (let event in events) {
el.addEventListener(event, events[event]);
}
return el;
};
module.exports.elementBefore = function elementBefore(html, before, events = {}) {
const el = _.element(html, null, events);
before.parentNode.insertBefore(el, before);
return el;
};
module.exports.noDefault = (f, ...args) => e => {
e.preventDefault();
const func = typeof f === 'function' ? f : _.get(Player, f);
func(e, ...args);
};
/***/ }),
/***/ "./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/actions.js":
/*!***********************************!*\
!*** ./src/components/actions.js ***!
\***********************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = {
atRoot: [ 'togglePlay', 'play', 'pause', 'next', 'previous', 'stop', 'toggleMute', 'volumeUp', 'volumeDown' ],
public: [ 'togglePlay', 'play', 'pause', 'next', 'previous', 'stop', 'toggleMute', 'volumeUp', 'volumeDown' ],
/**
* Switching being playing and paused.
*/
togglePlay: function () {
if (Player.audio.paused) {
Player.play();
} else {
Player.pause();
}
},
/**
* Start playback.
*/
play: async function (sound, { paused } = {}) {
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('canplaythrough', Player.actions._playOnceLoaded);
Player.audio.removeEventListener('canplaythrough', Player.actions._playOnceLoaded);
// If a new sound is being played update the display.
if (sound) {
if (Player.playing) {
Player.playing.playing = false;
}
// Remove audio events from the video, and add them back for standalone video.
const audioEvents = Player.controls.audioEvents;
for (let evt in audioEvents) {
let handlers = Array.isArray(audioEvents[evt]) ? audioEvents[evt] : [ audioEvents[evt] ];
handlers.forEach(handler => {
const handlerFunction = Player.getHandler(handler);
video.removeEventListener(evt, handlerFunction);
sound.standaloneVideo && video.addEventListener(evt, handlerFunction);
});
}
sound.playing = true;
Player.playing = sound;
Player.audio.src = sound.src;
Player.isVideo = sound.image.endsWith('.webm') || sound.type === 'video/webm';
Player.isStandalone = sound.standaloneVideo;
Player.audio = sound.standaloneVideo ? video : Player.controls._audio;
await Player.trigger('playsound', sound);
}
if (!paused) {
// If there's a video and sound wait for both to load before playing.
if (!Player.isStandalone && Player.isVideo && (video.readyState < 3 || Player.audio.readyState < 3)) {
video.addEventListener('canplaythrough', Player.actions._playOnceLoaded);
Player.audio.addEventListener('canplaythrough', Player.actions._playOnceLoaded);
} else {
Player.audio.play();
}
}
} catch (err) {
Player.logError('There was an error playing the sound. Please check the console for details.', 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('canplaythrough', Player.actions._playOnceLoaded);
Player.audio.removeEventListener('canplaythrough', Player.actions._playOnceLoaded);
Player.audio.play();
// Sometimes it just doesn't sync when the playback starts. Give it a second and then force a sync.
setTimeout(Player.controls.syncVideo, 100);
}
},
/**
* Pause playback.
*/
pause: function () {
Player.audio && Player.audio.pause();
},
/**
* Stop playback.
*/
stop: function () {
Player.audio.src = null;
Player.playing = null;
Player.trigger('stop');
},
/**
* Play the next sound.
*/
next: function (opts) {
Player.controls._movePlaying(1, opts);
},
/**
* Play the previous sound.
*/
previous: function (opts) {
Player.controls._movePlaying(-1, opts);
},
_movePlaying: function (direction, { force, group, paused } = {}) {
// 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.
let nextSound;
if (!force && Player.config.repeat === 'one') {
nextSound = Player.sounds[currentIndex];
} else {
let newIndex = currentIndex;
// Get the next index wrapping round if repeat all is selected
// Keep going if it's group move, there's still more sounds to check, and the next sound is still in the same group.
do {
newIndex = Player.config.repeat === 'all'
? ((newIndex + direction) + Player.sounds.length) % Player.sounds.length
: newIndex + direction;
nextSound = Player.sounds[newIndex];
} while (group && nextSound && newIndex !== currentIndex && (!nextSound.post || nextSound.post === Player.playing.post));
}
nextSound && Player.play(nextSound, { paused });
},
/**
* Raise the volume by 5%.
*/
volumeUp: function () {
Player.audio.volume = Math.min(Player.audio.volume + 0.05, 1);
},
/**
* Lower the volume by 5%.
*/
volumeDown: function () {
Player.audio.volume = Math.max(Player.audio.volume - 0.05, 0);
},
/**
* Mute the audio, or reset it to the last volume prior to muting.
*/
toggleMute: function () {
Player.audio.volume = (Player._lastVolume || 0.5) * !Player.audio.volume;
}
};
/***/ }),
/***/ "./src/components/colorpicker.js":
/*!***************************************!*\
!*** ./src/components/colorpicker.js ***!
\***************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {const HEIGHT = 200;
const WIDTH = 200;
module.exports = {
delegatedEvents: {
click: {
[`.${ns}-colorpicker-input, .${ns}-cp-preview`]: 'colorpicker.create',
[`.${ns}-apply-colorpicker`]: 'colorpicker._applyColorPicker',
[`.${ns}-close-colorpicker`]: _.noDefault(() => Player.display.closeDialogs())
},
change: {
[`.${ns}-rgb-input`]: 'colorpicker._handleRGBInput',
},
focusout: {
[`.${ns}-colorpicker-input`]: 'colorpicker._updatePreview'
},
mousedown: {
[`.${ns}-cp-saturation, .${ns}-cp-hue`]: e => {
const target = e.eventTarget;
target._mousedown = true;
document.documentElement.addEventListener('mouseup', _e => delete target._mousedown, { once: true });
Player.colorpicker[`_handle${e.eventTarget.classList.contains(`${ns}-cp-hue`) ? 'Hue' : 'Saturation'}Move`](e);
}
},
mousemove: {
[`.${ns}-cp-hue`]: 'colorpicker._handleHueMove',
[`.${ns}-cp-saturation`]: 'colorpicker._handleSaturationMove'
}
},
initialize: function () {
Player.on('menu-close', menu => menu._input && (delete menu._input._colorpicker));
},
create: function (e) {
e.preventDefault();
e.stopPropagation();
const parent = e.eventTarget.parentNode;
const input = e.eventTarget.nodeName === 'INPUT' ? e.eventTarget : parent.querySelector('input');
const preview = parent.querySelector(`.${ns}-cp-preview`);
if (!input || input._colorpicker) {
return;
}
Player.display.closeDialogs();
// Get the color from the preview.
const previewColor = window.getComputedStyle(preview).backgroundColor;
const rgbMatch = previewColor.match(/rgba?\((\d+), (\d+), (\d+)(?:, ([\d.]+))?\)/);
const rgb = [ +rgbMatch[1], +rgbMatch[2], +rgbMatch[3], isNaN(+rgbMatch[4]) ? 1 : rgbMatch[4] ];
const colorpicker = _.element(Player.templates.colorpicker({ HEIGHT, WIDTH, rgb }), parent);
Player.position.showRelativeTo(colorpicker, input);
input._colorpicker = colorpicker;
colorpicker._input = input;
colorpicker._colorpicker = { hsv: [ 0, 1, 1, 1 ], rgb: rgb };
// If there's a color in the input then update the hue/saturation positions to show it.
Player.colorpicker.updateOutput(colorpicker);
},
_handleHueMove: function (e) {
if (!e.eventTarget._mousedown) {
return;
}
e.preventDefault();
const colorpicker = e.eventTarget.closest(`.${ns}-colorpicker`);
const y = Math.max(0, e.clientY - e.eventTarget.getBoundingClientRect().top);
colorpicker._colorpicker.hsv[0] = y / HEIGHT;
const _hue = Player.colorpicker.hsv2rgb(colorpicker._colorpicker.hsv[0], 1, 1, 1);
colorpicker.querySelector(`.${ns}-cp-saturation`).style.background = `linear-gradient(to right, white, rgb(${_hue[0]}, ${_hue[1]}, ${_hue[2]}))`;
e.eventTarget.querySelector('.position').style.top = Math.max(-3, (y - 6)) + 'px';
Player.colorpicker.updateOutput(colorpicker, true);
},
_handleSaturationMove: function (e) {
if (!e.eventTarget._mousedown) {
return;
}
e.preventDefault();
const colorpicker = e.eventTarget.closest(`.${ns}-colorpicker`);
const saturationPosition = e.eventTarget.querySelector('.position');
const x = Math.max(0, e.clientX - e.eventTarget.getBoundingClientRect().left);
const y = Math.max(0, e.clientY - e.eventTarget.getBoundingClientRect().top);
colorpicker._colorpicker.hsv[1] = x / WIDTH;
colorpicker._colorpicker.hsv[2] = 1 - y / HEIGHT;
saturationPosition.style.top = Math.min(HEIGHT - 3, Math.max(-3, (y - 6))) + 'px';
saturationPosition.style.left = Math.min(WIDTH - 3, Math.max(-3, (x - 5))) + 'px';
Player.colorpicker.updateOutput(colorpicker, true);
},
_handleRGBInput: function (e) {
const colorpicker = e.eventTarget.closest(`.${ns}-colorpicker`);
colorpicker._colorpicker.rgb[+e.eventTarget.getAttribute('data-color')] = e.eventTarget.value;
Player.colorpicker.updateOutput(colorpicker);
},
updateOutput: function (colorpicker, fromHSV) {
const order = fromHSV ? [ 'hsv', 'rgb' ] : [ 'rgb', 'hsv' ];
colorpicker._colorpicker[order[1]] = Player.colorpicker[`${order[0]}2${order[1]}`](...colorpicker._colorpicker[order[0]]);
const [ r, g, b, a ] = colorpicker._colorpicker.rgb;
// Update the display.
if (fromHSV) {
colorpicker.querySelector(`.${ns}-rgb-input[data-color="0"]`).value = r;
colorpicker.querySelector(`.${ns}-rgb-input[data-color="1"]`).value = g;
colorpicker.querySelector(`.${ns}-rgb-input[data-color="2"]`).value = b;
colorpicker.querySelector(`.${ns}-rgb-input[data-color="3"]`).value = a;
} else {
const [ h, s, v ] = colorpicker._colorpicker.hsv;
const huePos = colorpicker.querySelector(`.${ns}-cp-hue .position`);
const satPos = colorpicker.querySelector(`.${ns}-cp-saturation .position`);
const _hue = Player.colorpicker.hsv2rgb(h, 1, 1, 1);
colorpicker.querySelector(`.${ns}-cp-saturation`).style.background = `linear-gradient(to right, white, rgb(${_hue[0]}, ${_hue[1]}, ${_hue[2]}))`;
huePos.style.top = (HEIGHT * h) - 3 + 'px';
satPos.style.left = (s * WIDTH) - 3 + 'px';
satPos.style.top = ((1 - v) * WIDTH) - 3 + 'px';
}
colorpicker.querySelector('.output-color').style.background = `rgb(${r}, ${g}, ${b}, ${a})`;
},
_applyColorPicker: function (e) {
e.preventDefault();
e.stopPropagation();
// Update the input.
const colorpicker = e.eventTarget.closest(`.${ns}-colorpicker`);
const [ r, g, b, a ] = colorpicker._colorpicker.rgb;
const input = colorpicker._input;
input.value = `rgb(${r}, ${g}, ${b}, ${a})`;
// Remove the colorpicker.
delete input._colorpicker;
colorpicker.parentNode.removeChild(colorpicker);
// Focus and blur to trigger the change handler.
input.focus();
input.blur();
},
hsv2rgb: function (h, s, v, a) {
const i = Math.floor((h * 6));
const f = (h * 6) - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
const mod = i % 6;
const r = [ v, q, p, p, t, v ][mod];
const g = [ t, v, v, q, p, p ][mod];
const b = [ p, p, t, v, v, q ][mod];
return [
Math.round(r * 255),
Math.round(g * 255),
Math.round(b * 255),
a
];
},
rgb2hsv: function (r, g, b, a) {
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const s = (max === 0 ? 0 : d / max);
const v = max / 255;
let h;
/* eslint-disable max-statements-per-line */
switch (max) {
case min: h = 0; break;
case r: h = (g - b) + d * (g < b ? 6 : 0); h /= 6 * d; break;
case g: h = (b - r) + d * 2; h /= 6 * d; break;
case b: h = (r - g) + d * 4; h /= 6 * d; break;
}
/* eslint-enable max-statements-per-line */
return [ h, s, v, a ];
},
_updatePreview: function (e) {
const value = e.eventTarget.value;
const preview = e.eventTarget.parentNode.querySelector(`.${ns}-cp-preview`);
preview.style.background = value;
}
};
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./src/components/controls.js":
/*!************************************!*\
!*** ./src/components/controls.js ***!
\************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {module.exports = {
delegatedEvents: {
click: {
[`.${ns}-previous-button`]: _.noDefault(() => Player.previous({ force: true })),
[`.${ns}-play-button`]: _.noDefault('togglePlay'),
[`.${ns}-next-button`]: _.noDefault(() => Player.next({ force: true })),
[`.${ns}-seek-bar`]: 'controls.handleSeek',
[`.${ns}-volume-bar`]: 'controls.handleVolume',
[`.${ns}-volume-button`]: _.noDefault('toggleMute'),
[`.${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', 'controls.preventWrapping' ],
durationchange: 'controls.updateDuration',
volumechange: 'controls.updateVolume',
loadstart: 'controls.pollForLoading',
error: 'controls.handleAudioError'
},
initialize: async function () {
// Keep this reference to switch Player.audio to standalone videos and back.
Player.controls._audio = Player.audio;
// Apply the previous volume
GM.getValue('volume').then(volume => volume >= 0 && volume <= 1 && (Player.audio.volume = volume));
// Only poll for the loaded data when the player is open.
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`);
// Set the initial volume/seek bar positions and hidden controls.
Player.controls.updateDuration();
Player.controls.updateVolume();
Player.controls.preventWrapping();
});
// Show all the controls when wrapping prevention is disabled.
Player.on('config:preventControlsWrapping', newValue => !newValue && Player.controls.showAllControls());
// Reset the hidden controls when the hide order is changed.
Player.on('config:controlsHideOrder', () => {
Player.controls.setHideOrder();
Player.controls.preventWrapping();
});
},
/**
* Handle audio errors
*/
handleAudioError: function (err) {
if (Player.playing) {
Player.logError(`Failed to play ${Player.playing.title}. Please check the console for details.`, err, 'warning');
Player.next();
}
},
/**
* Handle audio events. Sync the video up, and update the controls.
*/
handleAudioEvent: function () {
Player.controls.syncVideo();
Player.controls.updateDuration();
document.querySelectorAll(`.${ns}-play-button`).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.isVideo && !Player.isStandalone) {
const paused = Player.audio.paused;
const video = document.querySelector(`.${ns}-video`);
if (video) {
video.currentTime = Player.audio.currentTime % video.duration;
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 () {
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(Player.ui.currentTimeBar, Player.audio.currentTime, Player.audio.duration);
},
/**
* Update the volume bar.
*/
updateVolume: function () {
const vol = Player.audio.volume;
vol > 0 && (Player._lastVolume = vol);
GM.setValue('volume', vol);
document.querySelectorAll(`.${ns}-volume-button`).forEach(el => {
el.classList[vol === 0 ? 'add' : 'remove']('mute');
el.classList[vol > 0 ? 'add' : 'remove']('up');
});
Player.controls.updateProgressBarPosition(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 (bar, current, total) {
if (!bar) {
return;
}
current || (current = 0);
total || (total = 0);
const ratio = !total ? 0 : Math.max(0, Math.min(((current || 0) / total), 1));
bar.style.width = `calc(${ratio * 100}% - ${(0.8 * ratio) - 0.4}rem)`;
},
/**
* Handle the user interacting with the seek bar.
*/
handleSeek: function (e) {
e.preventDefault();
if (Player.audio.duration && Player.audio.duration !== Infinity) {
Player.audio.currentTime = Player.audio.duration * Player.controls._getBarXRatio(e);
}
},
/**
* Handle the user interacting with the volume bar.
*/
handleVolume: function (e) {
e.preventDefault();
Player.audio.volume = Player.controls._getBarXRatio(e);
Player.controls.updateVolume();
},
_getBarXRatio: function (e) {
const offset = 0.4 * Player.remSize;
return Math.max(0, Math.min(1, (e.offsetX - offset) / (parseInt(getComputedStyle(e.eventTarget || e.target).width, 10) - (2 * offset))));
},
/**
* Set all controls visible.
*/
showAllControls: function () {
Player.$all(`.${ns}-controls [data-hide-id]`).forEach(el => el.style.display = null);
},
/**
* Hide elements in the controls instead of wrapping
*/
preventWrapping: function () {
if (!Player.config.preventControlWrapping) {
return;
}
const controls = Player.$(`.${ns}-controls`);
// If the offset top of the last visible child than this value it indicates wrapping.
const expectedOffsetTop = parseFloat(window.getComputedStyle(controls).paddingTop);
const hideElements = Player.controls.hideOrder || Player.controls.setHideOrder();
let visibleChildren = Array.prototype.slice.call(controls.children);
let lastChild = visibleChildren.pop();
let hidden = 0;
// Show everything to check what has wrapped.
Player.controls.showAllControls();
// Keep hiding elements until the last visible child has not wrapped, or there's nothing left to hide.
while (lastChild.offsetTop > expectedOffsetTop && hidden < hideElements.length) {
const hide = hideElements[hidden++];
hide.style.display = 'none';
visibleChildren = visibleChildren.filter(el => el !== hide);
hide === lastChild && (lastChild = visibleChildren.pop());
}
},
/**
* Set the hide order from the user config.
*/
setHideOrder: function () {
if (!Array.isArray(Player.config.controlsHideOrder)) {
Player.settings.reset('controlsHideOrder');
}
const controls = Player.$(`.${ns}-controls`);
return Player.controls.hideOrder = Player.config.controlsHideOrder
.map(id => controls.querySelector(`[data-hide-id="${id}"]`))
.filter(el => el)
.sort((a, b) => a.dataset.hideOrder - b.dataset.hideOrder);
}
};
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./src/components/display.js":
/*!***********************************!*\
!*** ./src/components/display.js ***!
\***********************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {const selectors = __webpack_require__(/*! ../selectors */ "./src/selectors.js");
const settingsConfig = __webpack_require__(/*! config */ "./src/config/index.js");
const dismissedContentCache = {};
const dismissedRestoreCache = {};
module.exports = {
atRoot: [ 'show', 'hide' ],
public: [ 'show', 'hide' ],
delegatedEvents: {
click: {
[`.${ns}-close-button`]: 'hide',
[`.${ns}-dismiss-link`]: 'display._handleDismiss',
[`.${ns}-restore-link`]: 'display._handleRestore'
},
fullscreenchange: {
[`.${ns}-media`]: 'display._handleFullScreenChange'
},
drop: {
[`#${ns}-container`]: 'display._handleDrop'
}
},
undelegatedEvents: {
click: {
body: 'display.closeDialogs'
},
keydown: {
body: e => e.key === 'Escape' && Player.display.closeDialogs(e)
}
},
initialize: async function () {
try {
Player.display.dismissed = (await GM.getValue('dismissed')).split(',');
} catch (err) {
Player.display.dismissed = [];
}
Player.on('playsound', () => {
// Reset marquees
Player.display._marquees = {};
!Player.display._marqueeTO && Player.display.runTitleMarquee();
});
// Store the rem size
Player.remSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
},
/**
* Create the player show/hide button in to the 4chan X header.
*/
createPlayerButton: function () {
if (Site === 'FoolFuuka') {
// Add a sounds link in the nav for archives
const nav = document.querySelector('.navbar-inner .nav:nth-child(2)');
const li = _.element('<li><a href="javascript:;">Sounds</a></li>', nav);
li.children[0].addEventListener('click', Player.display.toggle);
} else if (Site === 'Fuuka') {
const br = document.querySelector('body > div > br');
br.parentNode.insertBefore(document.createTextNode('['), br);
_.elementBefore('<a href="javascript:;">Sounds</a>', br, { click: Player.display.toggle });
br.parentNode.insertBefore(document.createTextNode(']'), br);
} else if (isChanX) {
// Add a button in the header for 4chan X.
const showIcon = _.elementBefore(`<span id="shortcut-sounds" class="shortcut brackets-wrap" data-index="0">
<a href="javascript:;" title="Sounds" class="fa fa-play-circle">Sounds</a>
</span>`, document.getElementById('shortcut-settings'));
showIcon.querySelector('a').addEventListener('click', Player.display.toggle);
} else {
// Add a [Sounds] link in the top and bottom nav for native 4chan.
document.querySelectorAll('#settingsWindowLink, #settingsWindowLinkBot').forEach(function (link) {
_.elementBefore('<a href="javascript:;">Sounds</a>', link, { click: Player.display.toggle });
link.parentNode.insertBefore(document.createTextNode('] ['), link);
});
}
},
/**
* 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 = _.element(Player.templates.body(), parent);
Player.trigger('rendered');
} catch (err) {
Player.logError('There was an error rendering the sound player.', err);
// Can't recover, throw.
throw err;
}
},
forceBoardTheme: function () {
Player.display.applyBoardTheme(true);
Player.settings.save();
},
applyBoardTheme: function (force) {
// Create a reply element to gather the style from
const div = _.element(`<div class="${selectors.styleFetcher}"></div>`, 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, bypassStylesheet: 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();
},
updateStylesheet: function () {
// Add styles to handle 4chan X style not being available.
if (!isChanX) {
Player.chanXPFStylesheet = Player.chanXPFStylesheet || _.element('<style></style>', document.head);
Player.chanXPFStylesheet.innerHTML = Player.templates.css4chanXPolyfill();
}
// Insert the stylesheet if it doesn't exist.
Player.stylesheet = Player.stylesheet || _.element('<style></style>', 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) {
e && e.preventDefault();
Player.container.style.display = 'none';
Player.isHidden = true;
Player.trigger('hide');
},
/**
* Show the player. Reapplies the saved position/size, and resumes loaded amount polling if it was paused.
*/
show: async function (e) {
e && e.preventDefault();
if (!Player.container.style.display) {
return;
}
Player.container.style.display = null;
Player.isHidden = false;
await Player.trigger('show');
},
/**
* Stop playback and close the player.
*/
close: async function (e) {
Player.stop();
Player.hide();
},
/**
* 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();
}
Player.controls.preventWrapping();
},
_handleRestore: async function (e) {
e.preventDefault();
const restore = e.eventTarget.getAttribute('data-restore');
const restoreIndex = Player.display.dismissed.indexOf(restore);
if (restore && restoreIndex > -1) {
Player.display.dismissed.splice(restoreIndex, 1);
Player.$all(`[data-restore="${restore}"]`).forEach(el => {
_.elementBefore(dismissedContentCache[restore], el);
el.parentNode.removeChild(el);
});
await GM.setValue('dismissed', Player.display.dismissed.join(','));
}
},
_handleDismiss: async function (e) {
e.preventDefault();
const dismiss = e.eventTarget.getAttribute('data-dismiss');
if (dismiss && !Player.display.dismissed.includes(dismiss)) {
Player.display.dismissed.push(dismiss);
Player.$all(`[data-dismiss-id="${dismiss}"]`).forEach(el => {
_.elementBefore(`<a href="#" class="${ns}-restore-link" data-restore="${dismiss}">${dismissedRestoreCache[dismiss]}</a>`, el);
el.parentNode.removeChild(el);
});
await GM.setValue('dismissed', Player.display.dismissed.join(','));
}
},
ifNotDismissed: function (name, restore, text) {
dismissedContentCache[name] = text;
dismissedRestoreCache[name] = restore;
return Player.display.dismissed.includes(name)
? `<a href="#" class="${ns}-restore-link" data-restore="${name}">${restore}</a>`
: text;
},
/**
* Close any open menus.
*/
closeDialogs: function (e) {
document.querySelectorAll(`.${ns}-menu, .${ns}-colorpicker`).forEach(menu => {
// Don't close colorpickers when you click inside them.
if (!e || !menu.classList.contains(`${ns}-colorpicker`) || !menu.contains(e.target)) {
menu.parentNode.removeChild(menu);
Player.trigger('menu-close', menu);
}
});
},
runTitleMarquee: async function () {
Player.display._marqueeTO = setTimeout(Player.display.runTitleMarquee, 1000);
document.querySelectorAll(`.${ns}-title-marquee`).forEach(title => {
const offset = title.parentNode.getBoundingClientRect().width - (title.scrollWidth + 1);
const location = title.getAttribute('data-location');
// Fall out if the title is fully visible.
if (offset >= 0) {
return title.style.marginLeft = null;
}
const data = Player.display._marquees[location] = Player.display._marquees[location] || {
direction: 1,
position: parseInt(title.style.marginLeft, 10) || 0
};
// Pause at each end.
if (data.pause > 0) {
data.pause--;
return;
}
data.position -= (20 * data.direction);
// Pause then reverse direction when the end is reached.
if (data.position > 0 || data.position < offset) {
data.position = Math.min(0, Math.max(data.position, offset));
data.direction *= -1;
data.pause = 1;
}
title.style.marginLeft = data.position + 'px';
});
}
};
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./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) {
let handlers = Array.isArray(eventList[evt]) ? eventList[evt] : [ eventList[evt] ];
handlers.forEach(handler => Player.audio.addEventListener(evt, Player.getHandler(handler)));
}
}
});
},
/**
* 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.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.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);
}
}
};
/***/ }),
/***/ "./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 () {
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__(/*! config */ "./src/config/index.js");
module.exports = {
initialize: function () {
Player.on('rendered', Player.hotkeys.apply);
Player.on('config:hotkeys', Player.hotkeys.apply);
// Setup up hardware media keys.
if ('mediaSession' in navigator && Player.config.hardwareMediaKeys) {
const actions = [
[ 'play', () => Player.play() ],
[ 'pause', () => Player.pause() ],
[ 'stop', () => Player.pause() ],
[ 'previoustrack', () => Player.previous() ],
[ 'nexttrack', () => Player.next() ],
[ 'seekbackward', evt => Player.audio.currentTime -= evt.seekOffset || 10 ],
[ 'seekforward', evt => Player.audio.currentTime += evt.seekOffset || 10 ],
[ 'seekto', evt => Player.audio.currentTime += evt.seekTime ]
];
for (let [ type, handler ] of actions) {
try {
navigator.mediaSession.setActionHandler(type, handler);
} catch (err) {
// not enabled...
}
}
// Keep the media metadata updated.
Player.audio.addEventListener('pause', () => navigator.mediaSession.playbackState = 'paused');
Player.audio.addEventListener('ended', () => navigator.mediaSession.playbackState = 'paused');
Player.audio.addEventListener('play', function () {
navigator.mediaSession.playbackState = 'playing';
navigator.mediaSession.metadata = new MediaMetadata({
title: Player.playing.title,
artist: '4chan Sounds Player',
album: document.title,
artwork: [ { src: Player.playing.thumb } ]
});
});
}
},
_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 Player.getHandler(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;
}
};
/***/ }),
/***/ "./src/components/minimised.js":
/*!*************************************!*\
!*** ./src/components/minimised.js ***!
\*************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_, Icons) {module.exports = {
_showingPIP: false,
initialize: function () {
if (isChanX) {
Player.userTemplate.maintain(Player.minimised, 'chanXTemplate', [ 'chanXControls' ], [ 'show', 'hide', 'stop' ]);
}
Player.on('rendered', Player.minimised.render);
Player.on('show', Player.minimised.hidePIP);
Player.on('hide', Player.minimised.showPIP);
Player.on('stop', Player.minimised.hidePIP);
Player.on('playsound', 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 = _.elementBefore(`<span class="${ns}-chan-x-controls ${ns}-col-auto ${ns}-align-center"></span>`, 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,
location: '4chan-X-controls',
sound: Player.playing,
replacements: {
'prev-button': `<a href="#" class="${ns}-media-control ${ns}-previous-button ${ns}-hover-fill">${Icons.skipStart} ${Icons.skipStartFill}</a>`,
'play-button': `<a href="#" class="${ns}-media-control ${ns}-play-button ${ns}-hover-fill ${!Player.audio || Player.audio.paused ? `${ns}-play` : ''}">${Icons.play} ${Icons.pause} ${Icons.playFill} ${Icons.pauseFill}</a>`,
'next-button': `<a href="#" class="${ns}-media-control ${ns}-next-button ${ns}-hover-fill">${Icons.skipEnd} ${Icons.skipEndFill} </a>`,
'sound-current-time': `<span class="${ns}-current-time">0:00</span>`,
'sound-duration': `<span class="${ns}-duration">0:00</span>`
}
});
}
},
/**
* Move the image to a picture in picture like thumnail.
*/
showPIP: function () {
if (!Player.isHidden || !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);
}
};
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js"), __webpack_require__(/*! ./src/icons */ "./src/icons.js")))
/***/ }),
/***/ "./src/components/playlist.js":
/*!************************************!*\
!*** ./src/components/playlist.js ***!
\************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(Icons, _) {const { parseFiles, parseFileName } = __webpack_require__(/*! ../file_parser */ "./src/file_parser.js");
const { postIdPrefix } = __webpack_require__(/*! ../selectors */ "./src/selectors.js");
module.exports = {
atRoot: [ 'add', 'remove' ],
public: [ 'search' ],
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() },
keyup: { [`.${ns}-playlist-search`]: 'playlist._handleSearch' },
contextmenu: { [`.${ns}-list-item`]: 'playlist._handleItemMenu' }
},
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;
}
});
// Keey track of of the hover image element.
Player.on('rendered', () => Player.playlist.hoverImage = Player.$(`.${ns}-hover-image`));
// 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');
Player.config.autoScrollThread && sound.post && (location.href = location.href.split('#')[0] + '#' + postIdPrefix + sound.post);
});
// Reset to the placeholder image when the player is stopped.
Player.on('stop', () => {
Player.$all(`.${ns}-list-item.playing`).forEach(el => el.classList.remove('playing'));
Player.playlist.showImage({ image: `data:image/svg+xml;base64,${btoa(Icons.fcSounds)}` });
});
// 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);
// Listen to the search display being toggled
Player.on('config:showPlaylistSearch', Player.playlist.toggleSearch);
// Maintain changes to the user templates it's dependent values
Player.userTemplate.maintain(Player.playlist, 'rowTemplate', [ 'shuffle' ]);
},
/**
* Render the playlist.
*/
render: function () {
const container = Player.$(`.${ns}-list-container`);
container.innerHTML = Player.templates.list();
Player.events.addUndelegatedListeners(document.body, Player.playlist.undelegatedEvents);
Player.playlist.hoverImage = Player.$(`.${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) {
let isVideo = !thumb && (sound.image.endsWith('.webm') || sound.type === 'video/webm');
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');
},
/**
* Switch between playlist and image view.
*/
toggleView: function (e) {
e && e.preventDefault();
let style = Player.config.viewStyle === 'playlist' ? 'image'
: Player.config.viewStyle === 'image' ? 'playlist'
: Player.playlist._lastView;
Player.display.setViewStyle(style);
},
/**
* Add a new sound from the thread to the player.
*/
add: function (sound, skipRender) {
try {
const id = sound.id;
// Make sure the sound is not a duplicate.
if (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 = _.element(`<div>${Player.templates.list({ sounds: [ sound ] })}</div>`);
Player.events.addUndelegatedListeners(rowContainer, Player.playlist.undelegatedEvents);
if (index < Player.sounds.length - 1) {
const before = Player.$(`.${ns}-list-item[data-id="${Player.sounds[index + 1].id}"]`);
list.insertBefore(rowContainer.children[0], before);
} else {
list.appendChild(rowContainer.children[0]);
}
}
// If nothing else has been added yet show the image for this sound.
if (Player.sounds.length === 1) {
Player.playlist.showImage(sound);
}
// Auto show if enabled, we're on a thread, and this is the first non-standlone item.
if (Player.config.autoshow && /\/thread\//.test(location.href) && Player.sounds.filter(s => !s.standaloneVideo).length === 1) {
Player.show();
}
Player.trigger('add', sound);
}
} catch (err) {
Player.logError('There was an error adding to the sound player. Please check the console for details.', err);
console.log('[4chan sounds player]', sound);
}
},
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, null, true).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.next({ force: true, paused: Player.audio.paused });
}
// 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);
},
/**
* Display an item menu.
*/
_handleItemMenu: function (e) {
e.preventDefault();
e.stopPropagation();
const id = e.eventTarget.getAttribute('data-id');
const sound = Player.sounds.find(s => s.id === id);
// Add row item menus to the list container. Append to the container otherwise.
const listContainer = e.eventTarget.closest(`.${ns}-list-container`);
const parent = listContainer || Player.container;
// Create the menu.
const dialog = _.element(Player.templates.itemMenu({ sound, postIdPrefix }), parent);
const relative = e.eventTarget.classList.contains(`${ns}-item-menu-button`) ? e.eventTarget : e;
Player.userTemplate._showMenu(relative, dialog, parent);
},
/**
* 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}-player`);
const hideImage = !Player.config.hoverImages
|| Player.playlist._dragging
|| container.querySelector(`.${ns}-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}-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);
},
/**
* Search the playlist
*/
_handleSearch: function (e) {
Player.playlist.search(e.eventTarget.value.toLowerCase());
},
search: function (v) {
const lastSearch = Player.playlist._lastSearch;
Player.playlist._lastSearch = v;
if (v === lastSearch) {
return;
}
if (!v) {
return Player.$all(`.${ns}-list-item`).forEach(el => el.style.display = null);
}
Player.sounds.forEach(sound => {
const row = Player.$(`.${ns}-list-item[data-id="${sound.id}"]`);
row && (row.style.display = Player.playlist.matchesSearch(sound) ? null : 'none');
});
},
matchesSearch: function (sound) {
const v = Player.playlist._lastSearch;
return !v
|| sound.title.toLowerCase().includes(v)
|| String(sound.post.toLowerCase()).includes(v)
|| String(sound.src.toLowerCase()).includes(v);
},
toggleSearch: function (show) {
const input = Player.$(`.${ns}-playlist-search`);
!show && Player.playlist._lastSearch && Player.playlist.search();
input.style.display = show ? null : 'none';
show && input.focus();
}
};
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/icons */ "./src/icons.js"), __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./src/components/position.js":
/*!************************************!*\
!*** ./src/components/position.js ***!
\************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
const selectors = __webpack_require__(/*! ../selectors */ "./src/selectors.js");
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('position') || '').split(':');
const [ width, height ] = (await GM.getValue('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
});
// Listen for changes from other tabs
Player.syncTab('position', value => Player.position.move(...value.split(':').concat(true)));
Player.syncTab('size', value => Player.position.resize(...value.split(':')));
},
/**
* 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 enabled = !Player.isHidden && Player.config.limitPostWidths;
const startY = Player.container.offsetTop;
const endY = Player.container.getBoundingClientRect().height + startY;
document.querySelectorAll(selectors.limitWidthOf).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('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`)
: Player.config.viewStyle === 'tools' ? Player.$(`.${ns}-tools`) : null;
if (!heightElement) {
return;
}
const offset = Player.container.getBoundingClientRect().height - heightElement.getBoundingClientRect().height;
heightElement.style.height = (height - offset) + 'px';
Player.controls.preventWrapping();
},
/**
* 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('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 };
},
/**
* Position a fixed item with respect to an element or event.
*/
showRelativeTo: function (item, relative) {
// Try and put the item aligned to the left under the relative.
const relRect = relative instanceof Node
? relative.getBoundingClientRect()
: { top: relative.clientY, left: relative.clientX, width: 0, height: 0 };
item.style.top = relRect.top + relRect.height + 'px';
item.style.left = relRect.left + 'px';
// Reposition around the relative if the item is off screen.
const { width: width, height: height } = item.getBoundingClientRect();
if (relRect.left + width > document.documentElement.clientWidth) {
item.style.left = (relRect.left + relRect.width - width) + 'px';
}
if (relRect.top + relRect.height + height > document.documentElement.clientHeight - Player.position.getHeaderOffset().bottom) {
item.style.top = (relRect.top - height) + 'px';
}
}
};
/***/ }),
/***/ "./src/components/settings.js":
/*!************************************!*\
!*** ./src/components/settings.js ***!
\************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {const settingsConfig = __webpack_require__(/*! config */ "./src/config/index.js");
const migrations = __webpack_require__(/*! ../migrations */ "./src/migrations.js");
module.exports = {
atRoot: [ 'set' ],
changelog: 'https://github.com/rcc11/4chan-sounds-player/releases',
delegatedEvents: {
click: {
[`.${ns}-settings .${ns}-heading-action`]: 'settings._handleAction',
[`.${ns}-settings-tab`]: 'settings._handleTab'
},
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 () {
Player.settings.view = 'Display';
// Apply the default board theme as default.
Player.display.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();
if (Player.config.showUpdatedNotification && Player.config.VERSION && Player.config.VERSION !== "3.2.1") {
Player.alert(`4chan Sounds Player has been updated to <a href="${Player.settings.changelog}" target="_blank">version ${"3.2.1"}</a>.`);
}
// Run any migrations.
await Player.settings.migrate(Player.config.VERSION);
// Listen for the player closing to apply the pause on hide setting.
Player.on('hide', function () {
if (Player.config.pauseOnHide) {
Player.pause();
}
});
// Listen for changes from other tabs
Player.syncTab('settings', value => Player.settings.apply(value, {
bypassSave: true,
applyDefault: true,
ignore: [ 'viewStyle' ]
}));
},
render: function () {
if (Player.container) {
Player.$(`.${ns}-settings`).innerHTML = Player.templates.settings();
}
},
/**
* Update a setting.
*/
set: function (property, value, { bypassValidation, bypassSave, bypassRender, silent, bypassStylesheet, settingConfig } = {}) {
settingConfig = settingConfig || Player.settings.findDefault(property);
const previousValue = _.get(Player.config, property);
// Check if the value has actually changed.
if (!bypassValidation && _.isEqual(previousValue, value)) {
return;
}
// Set the new value.
_.set(Player.config, property, value);
// Trigger events, unless they are disabled in opts.
!bypassStylesheet && settingConfig && settingConfig.updateStylesheet && Player.display.updateStylesheet();
!silent && Player.trigger('config', property, value, previousValue);
!silent && Player.trigger('config:' + property, value, previousValue);
!bypassSave && Player.settings.save();
!bypassRender && settingConfig.displayGroup && Player.settings.render();
},
/**
* Reset a setting to the default value
*/
reset: function (property) {
let settingConfig = Player.settings.findDefault(property);
Player.set(property, settingConfig.default, { settingConfig });
},
/**
* Persist the player settings.
*/
save: function () {
try {
// Filter settings that haven't 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 {
let userVal = _.get(Player.config, setting.property);
if (userVal !== undefined && !_.isEqual(userVal, setting.default)) {
// If the setting is a mixed in object only store items that differ from the default.
if (setting.mix) {
userVal = Object.keys(userVal).reduce((changed, key) => {
if (!_.isEqual(setting.default[key], userVal[key])) {
changed[key] = userVal[key];
}
return changed;
}, {});
}
_.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 = "3.2.1";
// Save the settings.
return GM.setValue('settings', JSON.stringify(settings));
} catch (err) {
Player.logError('There was an error saving the sound player settings.', err);
}
},
/**
* Restore the saved player settings.
*/
load: async function () {
try {
let settings = await GM.getValue('settings') || await GM.getValue(ns + '.settings');
if (settings) {
Player.settings.apply(settings, { bypassSave: true, silent: true });
}
} catch (err) {
Player.logError('There was an error loading the sound player settings.', err);
}
},
apply: function (settings, opts = {}) {
if (typeof settings === 'string') {
settings = JSON.parse(settings);
}
settings.VERSION && (Player.config.VERSION = settings.VERSION);
settingsConfig.forEach(function _handleSetting(setting) {
if (setting.settings) {
return setting.settings.forEach(subSetting => _handleSetting({
property: setting.property,
default: setting.default,
...subSetting
}));
}
if (opts.ignore && opts.ignore.includes(setting.property)) {
return;
}
let value = _.get(settings, setting.property, opts.applyDefault ? setting.default : undefined);
if (value !== undefined) {
if (setting.mix) {
// Mix in default.
value = { ...setting.default, ...(value || {}) };
}
Player.set(setting.property, value, { ...opts, settingConfig: setting });
}
});
},
/**
* Run migrations when the player is updated.
*/
migrate: async function (fromVersion) {
// Fall out if the player hasn't updated.
if (!fromVersion || fromVersion === "3.2.1") {
return;
}
for (let i = 0; i < migrations.length; i++) {
let mig = migrations[i];
if (Player.settings.compareVersions(fromVersion, mig.version) < 0) {
try {
console.log('[4chan sound player] Migrate:', mig.name);
await mig.run();
} catch (err) {
console.error(err);
}
}
}
Player.settings.save();
},
/**
* Compare two semver strings.
*/
compareVersions: function (a, b) {
const [ aVer, aHash ] = a.split('-');
const [ bVer, bHash ] = b.split('-');
const aParts = aVer.split('.');
const bParts = bVer.split('.');
for (let i = 0; i < 3; i++) {
if (+aParts[i] > +bParts[i]) {
return 1;
}
if (+aParts[i] < +bParts[i]) {
return -1;
}
}
return aHash !== bHash;
},
/**
* 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 (group) {
// Blur anything focused so the change is applied.
let focused = Player.$(`.${ns}-settings :focus`);
focused && focused.blur();
// Restore the playlist if there's no group given and the settings are already open.
if (!group && Player.config.viewStyle === 'settings') {
return Player.playlist.restore();
}
// Switch to the settings view if it's not already showing.
if (Player.config.viewStyle !== 'settings') {
Player.display.setViewStyle('settings');
}
// Switch to a given group.
if (group && group !== Player.settings.view) {
Player.settings.showGroup(group);
}
},
/**
* Switch the displayed group
*/
_handleTab: function (e) {
const group = e.eventTarget.getAttribute('data-group');
if (group) {
e.preventDefault();
Player.settings.showGroup(group);
}
},
showGroup: function (group) {
Player.settings.view = group;
const currentGroup = Player.$(`.${ns}-settings-group.active`);
const currentTab = Player.$(`.${ns}-settings-tab.active`);
currentGroup && currentGroup.classList.remove('active');
currentTab && currentTab.classList.remove('active');
Player.$(`.${ns}-settings-group[data-group="${group}"]`).classList.add('active');
Player.$(`.${ns}-settings-tab[data-group="${group}"]`).classList.add('active');
},
/**
* 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 = Player.getHandler(settingConfig.parse)(newValue, currentValue, e);
}
if (settingConfig && settingConfig.split) {
newValue = newValue.split(decodeURIComponent(settingConfig.split));
}
// Not the most stringent check but enough to avoid some spamming.
if (!_.isEqual(currentValue, newValue, !settingConfig.looseCompare)) {
// Update the setting.
Player.set(property, newValue, { bypassValidation: true, bypassRender: true, settingConfig });
}
} catch (err) {
Player.logError('There was an error updating the setting.', 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, e);
},
renderHosts: function (_value) {
return `<div class="${ns}-host-inputs">`
+ Object.keys(Player.config.uploadHosts).map(Player.templates.hostInput).join('')
+ '</div>';
},
parseHosts: function (newValue, hosts, e) {
hosts = { ...hosts };
const container = e.eventTarget.closest(`.${ns}-host-input`);
let name = container.getAttribute('data-host-name');
let host = hosts[name] = { ...hosts[name] };
const changedField = e.eventTarget.getAttribute('name');
try {
// If the name was changed then reassign in hosts and update the data-host-name attribute.
if (changedField === 'name' && newValue !== name) {
if (!newValue || hosts[newValue]) {
throw new PlayerError('A unique name for the host is required.', 'warning');
}
container.setAttribute('data-host-name', newValue);
hosts[newValue] = host;
delete hosts[name];
name = newValue;
}
// Validate URL
if (changedField === 'url' || changedField === 'soundUrl') {
try {
(changedField === 'url' || newValue) && new URL(newValue);
} catch (err) {
throw new PlayerError('The value must be a valid URL.', 'warning');
}
}
// Parse the data
if (changedField === 'data') {
try {
newValue = JSON.parse(newValue);
} catch (err) {
throw new PlayerError('The data must be valid JSON.', 'warning');
}
}
if (changedField === 'headers') {
try {
newValue = newValue ? JSON.parse(newValue) : undefined;
} catch (err) {
throw new PlayerError('The headers must be valid JSON.', 'warning');
}
}
} catch (err) {
host.invalid = true;
container.classList.add('invalid');
throw err;
}
if (newValue === undefined) {
delete host[changedField];
} else {
host[changedField] = newValue;
}
try {
const soundUrlValue = container.querySelector('[name=soundUrl]').value;
const headersValue = container.querySelector('[name=headers]').value;
if (name
&& JSON.parse(container.querySelector('[name=data]').value)
&& new URL(container.querySelector('[name=url]').value)
&& (!soundUrlValue || new URL(soundUrlValue))
&& (!headersValue || JSON.parse(headersValue))) {
delete host.invalid;
container.classList.remove('invalid');
}
} catch (err) {
// leave it invalid
}
return hosts;
},
addUploadHost: function () {
const hosts = Player.config.uploadHosts;
const container = Player.$(`.${ns}-host-inputs`);
let name = 'New Host';
let i = 1;
while (Player.config.uploadHosts[name]) {
name = name + ' ' + ++i;
}
hosts[name] = { invalid: true, data: { file: '$file' } };
if (container.children[0]) {
_.elementBefore(Player.templates.hostInput(name), container.children[0]);
} else {
_.element(Player.templates.hostInput(name), container);
}
Player.settings.set('uploadHosts', hosts, { bypassValidation: true, bypassRender: true, silent: true });
},
removeHost: function (prop, e) {
const hosts = Player.config.uploadHosts;
const container = e.eventTarget.closest(`.${ns}-host-input`);
const name = container.getAttribute('data-host-name');
// For hosts in the defaults set null so we know to not include them on load
if (Player.settings.findDefault('uploadHosts').default[name]) {
hosts[name] = null;
} else {
delete hosts[name];
}
container.parentNode.removeChild(container);
Player.settings.set('uploadHosts', hosts, { bypassValidation: true, bypassRender: true });
},
setDefaultHost: function (_new, _current, e) {
const selected = e.eventTarget.closest(`.${ns}-host-input`).getAttribute('data-host-name');
if (selected === Player.config.defaultUploadHost) {
return selected;
}
Object.keys(Player.config.uploadHosts).forEach(name => {
const checkbox = Player.$(`.${ns}-host-input[data-host-name="${name}"] input[data-property="defaultUploadHost"]`);
checkbox && (checkbox.checked = name === selected);
});
return selected;
},
restoreDefaultHosts: function () {
Object.assign(Player.config.uploadHosts, Player.settings.findDefault('uploadHosts').default);
Player.set('uploadHosts', Player.config.uploadHosts, { bypassValidation: true });
}
};
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./src/components/threads.js":
/*!***********************************!*\
!*** ./src/components/threads.js ***!
\***********************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {const { parseFileName } = __webpack_require__(/*! ../file_parser */ "./src/file_parser.js");
const { get } = __webpack_require__(/*! ../api */ "./src/api.js");
const maxSavedBoards = 10;
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',
[`.${ns}-threads-view-style`]: 'threads.toggleView'
},
keyup: {
[`.${ns}-threads-filter`]: e => Player.threads.filter(e.eventTarget.value)
},
change: {
[`.${ns}-threads input[type=checkbox]`]: 'threads.toggleBoard'
}
},
initialize: async 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);
try {
const savedBoards = await GM.getValue('threads_board_selection');
savedBoards && (Player.threads.selectedBoards = savedBoards.split(','));
} catch (err) {
// Leave it defaulted to the current board.
}
},
/**
* 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();
},
/**
* Switch between board and table view.
*/
toggleView: function (e) {
e.preventDefault();
Player.set('threadsViewStyle', e.eventTarget.getAttribute('data-style'));
},
/**
* 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`);
list.innerHTML = '';
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}`;
_.element(`<div class="boardBanner"><div class="boardTitle">${boardTitle}</div></div>`, list);
// Add each thread for the board
const threads = Player.threads.displayThreads[board];
for (let i = 0; i < threads.length; i++) {
list.appendChild(Parser.buildHTMLFromJSON.call(Parser, threads[i], threads[i].board, true, true));
// Add a line under each thread
_.element('<hr style="clear: both">', list);
}
}
} catch (err) {
Player.logError('Unable to display the threads board view.', err, 'warning');
// If there was an error fall back to the table view.
Player.set('threadsViewStyle', 'table');
Player.renderThreads();
}
}
},
/**
* Render just the board selection.
*/
renderBoards: function () {
Player.$(`.${ns}-thread-board-list`).innerHTML = Player.templates.threadBoards();
},
/**
* Toggle the threads view.
*/
toggle: function (e) {
e && e.preventDefault();
if (Player.config.viewStyle === 'threads') {
Player.playlist.restore();
} else {
Player.display.setViewStyle('threads');
}
},
/**
* Switch between showing just the selected boards and all boards.
*/
toggleBoardList: function (e) {
e.preventDefault();
Player.threads.showAllBoards = !Player.threads.showAllBoards;
Player.$(`.${ns}-all-boards-link`).innerHTML = Player.threads.showAllBoards ? 'Selected Only' : 'Show All';
Player.threads.renderBoards();
},
/**
* Select/deselect a board.
*/
toggleBoard: async function (e) {
const board = e.eventTarget.value;
const selected = e.eventTarget.checked;
if (selected) {
!Player.threads.selectedBoards.includes(board) && Player.threads.selectedBoards.unshift(board);
} else {
Player.threads.selectedBoards = Player.threads.selectedBoards.filter(b => b !== board);
}
await GM.setValue('threads_board_selection', Player.threads.selectedBoards.slice(0, maxSavedBoards).join(','));
},
/**
* Fetch the board list from the 4chan API.
*/
fetchBoards: async function (fetchThreads) {
Player.threads.loading = true;
Player.threads.render();
Player.threads.boardList = (await get(boardsURL)).boards;
if (fetchThreads) {
Player.threads.fetch();
} else {
Player.threads.loading = false;
Player.threads.render();
}
},
/**
* Fetch the catalog for each selected board and search for sounds in OPs.
*/
fetch: async function (e) {
e && e.preventDefault();
Player.threads.loading = true;
Player.threads.render();
if (!Player.threads.boardList) {
try {
await Player.threads.fetchBoards();
} catch (err) {
return Player.logError('Failed fetching the boards list.', err);
}
}
const allThreads = [];
try {
await Promise.all(Player.threads.selectedBoards.map(async board => {
const boardConf = Player.threads.boardList.find(boardConf => boardConf.board === board);
if (!boardConf) {
return;
}
const pages = boardConf && await get(catalogURL.replace('%s', board));
(pages || []).forEach(({ page, threads }) => {
allThreads.push(...threads.map(thread => Object.assign(thread, { board, page, ws_board: boardConf.ws_board })));
});
}));
Player.threads.soundThreads = allThreads.filter(thread => {
const sounds = parseFileName(thread.filename, `https://i.4cdn.org/${thread.board}/${thread.tim}${thread.ext}`, thread.no, `https://i.4cdn.org/${thread.board}/${thread.tim}s${thread.ext}`, thread.md5, true);
return sounds.length;
});
} catch (err) {
Player.logError('Failed searching for sounds threads.', err);
}
Player.threads.loading = false;
Player.threads.filter(Player.$(`.${ns}-threads-filter`).value, true);
Player.threads.render();
},
/**
* Apply the filter input to the already fetched threads.
*/
filter: function (search, skipRender) {
search = search.toLowerCase();
Player.threads.filterValue = search || '';
if (Player.threads.soundThreads === null) {
return;
}
Player.threads.displayThreads = Player.threads.soundThreads.reduce((threadsByBoard, thread) => {
if (!search || thread.sub && thread.sub.toLowerCase().includes(search) || thread.com && thread.com.toLowerCase().includes(search)) {
threadsByBoard[thread.board] || (threadsByBoard[thread.board] = []);
threadsByBoard[thread.board].push(thread);
}
return threadsByBoard;
}, {});
!skipRender && Player.threads.renderThreads();
}
};
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./src/components/tools.js":
/*!*********************************!*\
!*** ./src/components/tools.js ***!
\*********************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_, Icons) {const ffmpegVersionUrl = 'https://raw.githubusercontent.com/rcc11/4chan-sounds-player/master/dist/4chan-sounds-player-with-ffmpeg.user.js';
const promoteFFmpegVersion = false;
// Seems to be the cut off point for file names
const maxFilenameLength = 218;
module.exports = {
hasFFmpeg: typeof ffmpeg === 'function',
_uploadIdx: 0,
createStatusText: '',
delegatedEvents: {
click: {
[`.${ns}-create-button`]: 'tools._handleCreate',
[`.${ns}-create-sound-post-link`]: 'tools._addCreatedToQR',
[`.${ns}-create-sound-add-link`]: 'tools._addCreatedToPlayer',
[`.${ns}-toggle-sound-input`]: 'tools._handleToggleSoundInput',
[`.${ns}-host-setting-link`]: _.noDefault(() => Player.settings.toggle('Hosts')),
[`.${ns}-remove-file`]: 'tools._handleFileRemove'
},
change: {
[`.${ns}-create-sound-img`]: 'tools._handleImageSelect',
[`.${ns}-create-sound-form input[type=file]`]: e => Player.tools._handleFileSelect(e.eventTarget),
[`.${ns}-use-video`]: 'tools._handleWebmSoundChange'
},
drop: {
[`.${ns}-create-sound-form`]: 'tools._handleCreateSoundDrop'
},
keyup: {
[`.${ns}-encoded-input`]: 'tools._handleEncoded',
[`.${ns}-decoded-input`]: 'tools._handleDecoded'
}
},
initialize: function () {
Player.on('config:uploadHosts', Player.tools.render);
Player.on('config:defaultUploadHost', newValue => Player.$(`.${ns}-create-sound-host`).value = newValue);
Player.on('rendered', Player.tools.afterRender);
},
render: function () {
Player.$(`.${ns}-tools`).innerHTML = Player.templates.tools();
Player.tools.afterRender();
},
afterRender: function () {
Player.tools.status = Player.$(`.${ns}-create-sound-status`);
Player.tools.imgInput = Player.$(`.${ns}-create-sound-img`);
Player.tools.sndInput = Player.$(`.${ns}-create-sound-snd`);
},
toggle: function (e) {
e && e.preventDefault();
if (Player.config.viewStyle === 'tools') {
Player.playlist.restore();
} else {
Player.display.setViewStyle('tools');
}
},
updateCreateStatus: function (text) {
Player.tools.status.style.display = text ? 'inherit' : 'none';
Player.tools.status.innerHTML = Player.tools.createStatusText = text;
},
/**
* Encode the decoded input.
*/
_handleDecoded: function (e) {
Player.$(`.${ns}-encoded-input`).value = encodeURIComponent(e.eventTarget.value);
},
/**
* Decode the encoded input.
*/
_handleEncoded: function (e) {
Player.$(`.${ns}-decoded-input`).value = decodeURIComponent(e.eventTarget.value);
},
/**
* Show/hide the "Use webm" checkbox when an image is selected.
*/
_handleImageSelect: async function (e) {
const input = e && e.eventTarget || Player.tools.imgInput;
const image = input.files[0];
const isVideo = image.type === 'video/webm';
let placeholder = image.name.replace(/\.[^/.]+$/, '');
if (Player.tools.hasFFmpeg) {
// Show the Use Webm label if the image is a webm file
Player.$(`.${ns}-use-video-label`).style.display = isVideo ? 'inherit' : 'none';
const webmCheckbox = Player.$(`.${ns}-use-video`);
// If the image is a video and Copy Video is selected then update the sound input as well
webmCheckbox.checked && isVideo && Player.tools._handleFileSelect(Player.tools.sndInput, [ image ]);
// If the image isn't a webm make sure Copy Video is deselected (click to fire change event)
webmCheckbox.checked && !isVideo && webmCheckbox.click();
} else if (await Player.tools.hasAudio(image)) {
Player.logError('Audio not allowed for the image webm.', null, 'warning');
}
// Show the image name as the placeholder for the name input since it's the default
Player.$(`.${ns}-create-sound-name`).setAttribute('placeholder', placeholder);
},
/**
* Update the custom file input display when the input changes
*/
_handleFileSelect: function (input, files) {
const container = input.closest(`.${ns}-file-input`);
const fileText = container.querySelector('.text');
const fileList = container.querySelector(`.${ns}-file-list`);
files || (files = [ ...input.files ]);
container.classList[files.length ? 'remove' : 'add']('placeholder');
fileText.innerHTML = files.length > 1
? files.length + ' files'
: files[0] && files[0].name || '';
fileList && (fileList.innerHTML = files.length < 2 ? '' : files.map((file, i) =>
`<div class="${ns}-row">
<div class="${ns}-col ${ns}-truncate-text">${file.name}</div>
<a class="${ns}-col-auto ${ns}-remove-file" href="#" data-idx="${i}">${Icons.close}</a>
</div>`
).join(''));
},
/**
* Handle a file being removed from a multi input
*/
_handleFileRemove: function (e) {
e.preventDefault();
const idx = +e.eventTarget.getAttribute('data-idx');
const input = e.eventTarget.closest(`.${ns}-file-input`).querySelector('input[type="file"]');
const dataTransfer = new DataTransfer();
for (let i = 0; i < input.files.length; i++) {
i !== idx && dataTransfer.items.add(input.files[i]);
}
input.files = dataTransfer.files;
Player.tools._handleFileSelect(input);
},
/**
* Show/hide the sound input when "Use webm" is changed.
*/
_handleWebmSoundChange: function (e) {
const sound = Player.tools.sndInput;
const image = Player.tools.imgInput;
Player.tools._handleFileSelect(sound, e.eventTarget.checked && [ image.files[0] ]);
},
_handleToggleSoundInput: function (e) {
e.preventDefault();
const showURL = e.eventTarget.getAttribute('data-type') === 'url';
Player.$(`.${ns}-create-sound-snd-url`).closest(`.${ns}-row`).style.display = showURL ? null : 'none';
Player.$(`.${ns}-create-sound-snd`).closest(`.${ns}-file-input`).style.display = showURL ? 'none' : null;
Player.tools.useSoundURL = showURL;
},
/**
* Handle files being dropped on the create sound section.
*/
_handleCreateSoundDrop: function (e) {
e.preventDefault();
e.stopPropagation();
const targetInput = e.target.nodeName === 'INPUT' && e.target.getAttribute('type') === 'file' && e.target;
[ ...e.dataTransfer.files ].forEach(file => {
const isVideo = file.type.startsWith('video');
const isImage = file.type.startsWith('image') || file.type === 'video/webm';
const isSound = file.type.startsWith('audio');
if (isVideo || isImage || isSound) {
const input = file.type === 'video/webm' && targetInput
? targetInput
: isImage
? Player.tools.imgInput
: Player.tools.sndInput;
const dataTransfer = new DataTransfer();
if (input.multiple) {
[ ...input.files ].forEach(file => dataTransfer.items.add(file));
}
dataTransfer.items.add(file);
input.files = dataTransfer.files;
Player.tools._handleFileSelect(input);
input === Player.tools.imgInput && Player.tools._handleImageSelect();
}
});
return false;
},
/**
* Handle the create button.
* Extracts video/audio if required, uploads the sound, and creates an image file names with [sound=url].
*/
_handleCreate: async function (e) {
e && e.preventDefault();
// Revoke the URL for an existing created image.
Player.tools._createdImageURL && URL.revokeObjectURL(Player.tools._createdImageURL);
Player.tools._createdImage = null;
Player.tools.updateCreateStatus('Creating sound image');
Player.$(`.${ns}-create-button`).disabled = true;
// Gather the input values.
const host = Player.config.uploadHosts[Player.$(`.${ns}-create-sound-host`).value];
const useSoundURL = Player.tools.useSoundURL;
let image = Player.tools.imgInput.files[0];
let soundURLs = useSoundURL && Player.$(`.${ns}-create-sound-snd-url`).value.split(',').map(v => v.trim()).filter(v => v);
let sounds = !(Player.$(`.${ns}-use-video`) || {}).checked || !image || !image.type.startsWith('video')
? [ ...Player.tools.sndInput.files ]
: image && [ image ];
const customName = Player.$(`.${ns}-create-sound-name`).value;
// Only split a given name if there's multiple sounds.
const names = customName
? ((soundURLs || sounds).length > 1 ? customName.split(',') : [ customName ]).map(v => v.trim())
: image && [ image.name.replace(/\.[^/.]+$/, '') ];
try {
if (!image) {
throw new PlayerError('Select an image or webm.', 'warning');
}
if (image.type.startsWith('video') && await Player.tools.hasAudio(image)) {
// If ffmpeg is not available fall out.
if (!Player.tools.hasFFmpeg) {
Player.tools.updateCreateStatus(Player.tools.createStatusText
+ '<br>' + (promoteFFmpegVersion ? 'This version of the player does not enable webm splitting.' : 'Audio not allowed for the image webm.')
+ '<br>Remove the audio from the webm and try again.'
+ (promoteFFmpegVersion ? `<br>Alternatively install the <a href="${ffmpegVersionUrl}">ffmpeg version</a> to extract video/audio automatically.` : ''));
throw new PlayerError('Audio not allowed for the image webm.', 'warning');
}
// If the image is a webm with audio then extract just the video.
image = await Player.tools.extract(image, 'video');
}
const soundlessLength = names.join('').length + (soundURLs || sounds).length * 8;
if (useSoundURL) {
try {
// Make sure each url is valid and strip the protocol.
soundURLs = soundURLs.map(url => new URL(url) && url.replace(/^(https?:)?\/\//, ''));
} catch (err) {
throw new PlayerError('The provided sound URL is invalid.', 'warning');
}
if (maxFilenameLength < soundlessLength + soundURLs.join('').length) {
throw new PlayerError('The generated image filename is too long.', 'warning');
}
} else {
if (!sounds || !sounds.length) {
throw new PlayerError('Select a sound.', 'warning');
}
// Check the final filename length if the URL length is known for the host.
// Limit to 8 otherwise. zz.ht is as small as you're likely to get and that can only fit 8.
const tooManySounds = host.filenameLength
? maxFilenameLength < soundlessLength + (host.filenameLength) * sounds.length
: sounds.length > 8;
if (tooManySounds) {
throw new PlayerError('The generated image filename is too long.', 'warning');
}
// Check videos have audio and extract it if possible.
sounds = await Promise.all(sounds.map(async sound => {
if (sound.type.startsWith('video')) {
if (!await Player.tools.hasAudio(sound)) {
throw new PlayerError(`The selected video has no audio. (${sound.name})`, 'warning');
}
if (Player.tools.hasFFmpeg) {
return await Player.tools.extract(sound, 'audio');
}
}
return sound;
}));
// Upload the sounds.
try {
soundURLs = await Promise.all(sounds.map(async sound => Player.tools.postFile(sound, host)));
} catch (err) {
throw new PlayerError('Upload failed.', 'error', err);
}
}
if (!soundURLs.length) {
throw new PlayerError('No sounds selected.', 'warning');
}
// Create a new file that includes [sound=url] in the name.
let filename = '';
for (let i = 0; i < soundURLs.length; i++) {
filename += (names[i] || '') + '[sound=' + encodeURIComponent(soundURLs[i].replace(/^(https?:)?\/\//, '')) + ']';
}
const ext = image.name.match(/\.([^/.]+)$/)[1];
const soundImage = new File([ image ], filename + '.' + ext, { type: image.type });
// Keep track of the create image and a url to it.
Player.tools._createdImage = soundImage;
Player.tools._createdImageURL = URL.createObjectURL(soundImage);
// Complete! with some action links
Player.tools.updateCreateStatus(Player.tools.createStatusText
+ '<br>Complete!<br>'
+ (is4chan ? `<a href="#" class="${ns}-create-sound-post-link">Post</a> - ` : '')
+ ` <a href="#" class="${ns}-create-sound-add-link">Add</a> - `
+ ` <a href="${Player.tools._createdImageURL}" download="${soundImage.name}" title="${soundImage.name}">Download</a>`
);
} catch (err) {
Player.tools.updateCreateStatus(Player.tools.createStatusText
+ '<br>Failed! ' + (err instanceof PlayerError ? err.reason : ''));
Player.logError('Failed to create sound image', err);
}
Player.$(`.${ns}-create-button`).disabled = false;
},
hasAudio: function (file) {
if (!file.type.startsWith('audio') && !file.type.startsWith('video')) {
return false;
}
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const video = document.createElement('video');
video.addEventListener('loadeddata', () => {
URL.revokeObjectURL(url);
resolve(video.mozHasAudio || !!video.webkitAudioDecodedByteCount);
});
video.addEventListener('error', reject);
video.src = url;
});
},
/**
* Extract just the audio or video from a file.
*/
extract: async function (file, type) {
Player.tools.updateCreateStatus(Player.tools.createStatusText + '<br>Extracting ' + type);
if (typeof ffmpeg !== 'function') {
return file;
}
const name = file.name.replace(/\.[^/.]+$/, '') + (type === 'audio' ? '.ogg' : '.webm');
const result = ffmpeg({
MEMFS: [ { name: '_' + file.name, data: await new Response(file).arrayBuffer() } ],
arguments: type === 'audio'
? [ '-i', '_' + file.name, '-vn', '-c', 'copy', name ]
: [ '-i', '_' + file.name, '-an', '-c', 'copy', name ]
});
return new File([ result.MEMFS[0].data ], name, { type: type === 'audio' ? 'audio/ogg' : 'video/webm' });
},
/**
* Upload the sound file and return a link to it.
*/
postFile: async function (file, host) {
const idx = Player.tools._uploadIdx++;
if (!host || host.invalid) {
throw new PlayerError('Invalid upload host.', 'error');
}
const formData = new FormData();
Object.keys(host.data).forEach(key => {
if (host.data[key] !== null) {
formData.append(key, host.data[key] === '$file' ? file : host.data[key]);
}
});
Player.tools.updateCreateStatus(Player.tools.createStatusText + `<br><span class="${ns}-upload-status-${idx}">Uploading ${file.name}</span>`);
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'POST',
url: host.url,
data: formData,
responseType: host.responsePath ? 'json' : 'text',
headers: host.headers,
onload: async response => {
if (response.status < 200 || response.status >= 300) {
return reject(response);
}
const responseVal = host.responsePath
? _.get(response.response, host.responsePath)
: host.responseMatch
? (response.responseText.match(new RegExp(host.responseMatch)) || [])[1]
: response.responseText;
const uploadedUrl = host.soundUrl ? host.soundUrl.replace('%s', responseVal) : responseVal;
Player.$(`.${ns}-upload-status-${idx}`).innerHTML = `Uploaded ${file.name} to <a href="${uploadedUrl}" target="_blank">${uploadedUrl}</a>`;
Player.tools.createStatusText = Player.tools.status.innerHTML;
resolve(uploadedUrl);
},
upload: {
onprogress: response => {
const total = response.total > 0 ? response.total : file.size;
Player.$(`.${ns}-upload-status-${idx}`).innerHTML = `Uploading ${file.name} - ${Math.floor(response.loaded / total * 100)}%`;
}
},
onerror: reject
});
});
},
/**
* Add the created sound image to the player.
*/
_addCreatedToPlayer: function (e) {
e.preventDefault();
Player.playlist.addFromFiles([ Player.tools._createdImage ]);
},
/**
* Open the QR window and add the created sound image to it.
*/
_addCreatedToQR: function (e) {
if (!is4chan) {
return;
}
e.preventDefault();
// Open the quick reply window.
const qrLink = document.querySelector(isChanX ? '.qr-link' : '.open-qr-link');
const dataTransfer = new DataTransfer();
dataTransfer.items.add(Player.tools._createdImage);
// 4chan X, drop the file on the qr window.
if (isChanX) {
qrLink.click();
const event = new CustomEvent('drop', { view: window, bubbles: true, cancelable: true });
event.dataTransfer = dataTransfer;
document.querySelector('#qr').dispatchEvent(event);
// Native, set the file input value. Check for a quick reply
} else if (qrLink) {
qrLink.click();
document.querySelector('#qrFile').files = dataTransfer.files;
} else {
document.querySelector('#togglePostFormLink a').click();
document.querySelector('#postFile').files = dataTransfer.files;
document.querySelector('.postForm').scrollIntoView();
}
},
};
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js"), __webpack_require__(/*! ./src/icons */ "./src/icons.js")))
/***/ }),
/***/ "./src/components/user-template/buttons.js":
/*!*************************************************!*\
!*** ./src/components/user-template/buttons.js ***!
\*************************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(Icons) {const { postIdPrefix } = __webpack_require__(/*! ../../selectors */ "./src/selectors.js");
module.exports = [
{
property: 'repeat',
tplName: 'repeat',
class: `${ns}-repeat-button`,
values: {
all: { attrs: [ 'title="Repeat All"' ], icon: Icons.arrowRepeat },
one: { attrs: [ 'title="Repeat One"' ], icon: Icons.arrowClockwise },
none: { attrs: [ 'title="No Repeat"' ], class: 'muted', icon: Icons.arrowRepeat }
}
},
{
property: 'shuffle',
tplName: 'shuffle',
class: `${ns}-shuffle-button`,
values: {
true: { attrs: [ 'title="Shuffled"' ], icon: Icons.shuffle },
false: { attrs: [ 'title="Ordered"' ], class: 'muted', icon: Icons.shuffle }
}
},
{
property: 'viewStyle',
tplName: 'playlist',
class: `${ns}-viewStyle-button`,
values: {
default: { attrs: [ 'title="Player"' ], class: 'muted', icon: () => (Player.playlist._lastView === 'playlist' ? Icons.arrowsExpand : Icons.arrowsCollapse) },
playlist: { attrs: [ 'title="Hide Playlist"' ], icon: Icons.arrowsExpand },
image: { attrs: [ 'title="Show Playlist"' ], icon: Icons.arrowsCollapse }
}
},
{
property: 'hoverImages',
tplName: 'hover-images',
class: `${ns}-hoverImages-button`,
values: {
true: { attrs: [ 'title="Hover Images Enabled"' ], icon: Icons.image },
false: { attrs: [ 'title="Hover Images Disabled"' ], class: 'muted', icon: Icons.image }
}
},
{
tplName: 'add',
class: `${ns}-add-button`,
icon: Icons.plus,
attrs: [ 'title="Add local files"' ]
},
{
tplName: 'reload',
class: `${ns}-reload-button`,
icon: Icons.reboot,
attrs: [ 'title="Reload the playlist"' ]
},
{
property: 'viewStyle',
tplName: 'settings',
class: `${ns}-config-button`,
icon: Icons.gear,
attrs: [ 'title="Settings"' ],
values: {
default: { class: 'muted' },
settings: { }
}
},
{
property: 'viewStyle',
tplName: 'threads',
class: `${ns}-threads-button`,
icon: Icons.search,
attrs: [ 'title="Threads"' ],
values: {
default: { class: 'muted' },
threads: { }
}
},
{
property: 'viewStyle',
tplName: 'tools',
class: `${ns}-tools-button`,
icon: Icons.tools,
attrs: [ 'title="Tools"' ],
values: {
default: { class: 'muted' },
tools: { }
}
},
{
tplName: 'close',
class: `${ns}-close-button`,
icon: Icons.close,
attrs: [ 'title="Hide the player"' ]
},
{
tplName: 'playing',
requireSound: true,
class: `${ns}-playing-jump-link`,
icon: Icons.musicNoteList,
attrs: [ 'title="Scroll the playlist currently playing sound."' ]
},
{
tplName: 'post',
requireSound: true,
icon: Icons.chatRightQuote,
showIf: data => data.sound.post,
attrs: data => [
`href=${'#' + postIdPrefix + data.sound.post}`,
'title="Jump to the post for the current sound"'
]
},
{
tplName: 'image',
requireSound: true,
icon: Icons.image,
attrs: data => [
`href=${data.sound.image}`,
'title="Open the image in a new tab"',
'target="_blank"'
]
},
{
tplName: 'sound',
requireSound: true,
href: data => data.sound.src,
icon: Icons.soundwave,
attrs: data => [
`href=${data.sound.src}`,
'title="Open the sound in a new tab"',
'target="_blank"'
]
},
{
tplName: 'dl-image',
requireSound: true,
class: `${ns}-download-link`,
icon: Icons.fileEarmarkImage,
attrs: data => [
'title="Download the image with the original filename"',
`data-src="${data.sound.image}"`,
`data-name="${data.sound.filename}"`
]
},
{
tplName: 'dl-sound',
requireSound: true,
class: `${ns}-download-link`,
icon: Icons.fileEarmarkMusic,
attrs: data => [
'title="Download the sound"',
`data-src="${data.sound.src}"`,
`data-name="${data.sound.name}"`
]
},
{
tplName: 'filter-image',
requireSound: true,
class: `${ns}-filter-link`,
icon: Icons.filter,
showIf: data => data.sound.imageMD5,
attrs: data => [
'title="Add the image MD5 to the filters."',
`data-filter="${data.sound.imageMD5}"`
]
},
{
tplName: 'filter-sound',
requireSound: true,
class: `${ns}-filter-link`,
icon: Icons.filter,
attrs: data => [
'title="Add the sound URL to the filters."',
`data-filter="${data.sound.src.replace(/^(https?:)?\/\//, '')}"`
]
},
{
tplName: 'remove',
requireSound: true,
class: `${ns}-remove-link`,
icon: Icons.trash,
attrs: data => [
'title="Filter the image."',
`data-id="${data.sound.id}"`
]
},
{
tplName: 'menu',
requireSound: true,
class: `${ns}-item-menu-button`,
icon: Icons.chevronDown,
attrs: data => [ `data-id=${data.sound.id}` ]
},
{
tplName: 'view-menu',
class: `${ns}-view-menu-button`,
icon: Icons.chevronDown,
attrs: [ 'href="javascript:;"' ]
}
];
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/icons */ "./src/icons.js")))
/***/ }),
/***/ "./src/components/user-template/index.js":
/*!***********************************************!*\
!*** ./src/components/user-template/index.js ***!
\***********************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {const buttons = __webpack_require__(/*! ./buttons */ "./src/components/user-template/buttons.js");
// Regex for replacements
const playingRE = /p: ?{([^}]*)}/g;
const hoverRE = /h: ?{([^}]*)}/g;
const buttonRE = new RegExp(`(${buttons.map(option => option.tplName).join('|')})-(?:button|link)(?:\\:"([^"]+?)")?`, 'g');
const soundNameRE = /sound-name/g;
const soundNameMarqueeRE = /sound-name-marquee/g;
const soundIndexRE = /sound-index/g;
const soundCountRE = /sound-count/g;
// Hold information on which config values components templates depend on.
const componentDeps = [ ];
module.exports = {
buttons,
delegatedEvents: {
click: {
[`.${ns}-playing-jump-link`]: () => Player.playlist.scrollToPlaying('center'),
[`.${ns}-viewStyle-button`]: 'playlist.toggleView',
[`.${ns}-hoverImages-button`]: 'playlist.toggleHoverImages',
[`.${ns}-remove-link`]: 'userTemplate._handleRemove',
[`.${ns}-filter-link`]: 'userTemplate._handleFilter',
[`.${ns}-download-link`]: 'userTemplate._handleDownload',
[`.${ns}-shuffle-button`]: 'userTemplate._handleShuffle',
[`.${ns}-repeat-button`]: 'userTemplate._handleRepeat',
[`.${ns}-reload-button`]: _.noDefault('playlist.refresh'),
[`.${ns}-add-button`]: _.noDefault(() => Player.$(`.${ns}-add-local-file-input`).click()),
[`.${ns}-item-menu-button`]: 'playlist._handleItemMenu',
[`.${ns}-view-menu-button`]: 'userTemplate._handleViewsMenu',
[`.${ns}-threads-button`]: 'threads.toggle',
[`.${ns}-tools-button`]: 'tools.toggle',
[`.${ns}-config-button`]: _.noDefault(() => Player.settings.toggle()),
[`.${ns}-favorites-button`]: 'favorites.toggle',
[`.${ns}-player-button`]: 'playlist.restore'
},
change: {
[`.${ns}-add-local-file-input`]: 'userTemplate._handleFileSelect'
}
},
initialize: function () {
Player.on('config', Player.userTemplate._handleConfig);
Player.on('playsound', () => Player.userTemplate._handleEvent('playsound'));
[ 'add', 'remove', 'order', 'show', 'hide', 'stop' ].forEach(evt => {
Player.on(evt, Player.userTemplate._handleEvent.bind(null, evt));
});
},
/**
* Build a user template.
*/
build: function (data) {
const outerClass = data.outerClass || '';
const name = data.sound && data.sound.title || data.defaultName;
const _confFuncOrText = v => (typeof v === 'function' ? v(data) : v);
// Apply common template replacements
let html = data.template
.replace(playingRE, Player.playing && Player.playing === data.sound ? '$1' : '')
.replace(hoverRE, `<span class="${ns}-hover-display ${outerClass}">$1</span>`)
.replace(buttonRE, function (full, type, text) {
let buttonConf = buttons.find(conf => conf.tplName === type);
if (buttonConf.requireSound && !data.sound || buttonConf.showIf && !buttonConf.showIf(data)) {
return '';
}
// If the button config has sub values then extend the base config with the selected sub value.
// Which value is to use is taken from the `property` in the base config of the player config.
// This gives us different state displays.
if (buttonConf.values) {
let topConf = buttonConf;
const valConf = buttonConf.values[_.get(Player.config, buttonConf.property)] || buttonConf.values[Object.keys(buttonConf.values)[0]];
buttonConf = { ...topConf, ...valConf, class: ((topConf.class || '') + ' ' + (valConf.class || '')).trim() };
}
const attrs = [ ...(_confFuncOrText(buttonConf.attrs) || []) ];
attrs.some(attr => attr.startsWith('href')) || attrs.push('href="javascript:;"');
(buttonConf.class || outerClass) && attrs.push(`class="${buttonConf.class || ''} ${outerClass || ''}"`);
return `<a ${attrs.join(' ')}>${text || _confFuncOrText(buttonConf.icon) || _confFuncOrText(buttonConf.text)}</a>`;
})
.replace(soundNameMarqueeRE, name ? `<div class="${ns}-col ${ns}-truncate-text" style="margin: 0 .5rem; text-overflow: clip;"><span title="${name}" class="${ns}-title-marquee" data-location="${data.location || ''}">${name}</span></div>` : '')
.replace(soundNameRE, name ? `<div class="${ns}-col ${ns}-truncate-text" style="margin: 0 .5rem"><span title="${name}">${name}</span></div>` : '')
.replace(soundIndexRE, data.sound ? Player.sounds.indexOf(data.sound) + 1 : 0)
.replace(soundCountRE, Player.sounds.length)
.replace(/%v/g, "3.2.1");
// Apply any specific replacements
if (data.replacements) {
for (let k of Object.keys(data.replacements)) {
html = html.replace(new RegExp(k, 'g'), data.replacements[k]);
}
}
return html;
},
/**
* Sets up a components to render when the template or values within it are changed.
*/
maintain: function (component, property, alwaysRenderConfigs = [], alwaysRenderEvents = []) {
componentDeps.push({
component,
property,
...Player.userTemplate.findDependencies(property, null),
alwaysRenderConfigs,
alwaysRenderEvents
});
},
/**
* Find all the config dependent values in a template.
*/
findDependencies: function (property, template) {
template || (template = _.get(Player.config, property));
// Figure out what events should trigger a render.
const events = [];
// add/remove should render templates showing the count.
// playsound/stop should render templates showing the playing sounds name/index or dependent on something playing.
// order should render templates showing a sounds index.
const hasCount = soundCountRE.test(template);
const hasName = soundNameRE.test(template);
const hasIndex = soundIndexRE.test(template);
const hasPlaying = playingRE.test(template);
hasCount && events.push('add', 'remove');
(hasPlaying || property !== 'rowTemplate' && (hasName || hasIndex)) && events.push('playsound', 'stop');
hasIndex && events.push('order');
// Find which buttons the template includes that are dependent on config values.
const config = [];
let match;
while ((match = buttonRE.exec(template)) !== null) {
// If user text is given then the display doesn't change.
if (!match[2]) {
let type = match[1];
let buttonConf = buttons.find(conf => conf.tplName === type);
if (buttonConf.property) {
config.push(buttonConf.property);
}
}
}
return { events, config };
},
/**
* When a config value is changed check if any component dependencies are affected.
*/
_handleConfig: function (property, value) {
// Check if a template for a components was updated.
componentDeps.forEach(depInfo => {
if (depInfo.property === property) {
Object.assign(depInfo, Player.userTemplate.findDependencies(property, value));
depInfo.component.render();
}
});
// Check if any components are dependent on the updated property.
componentDeps.forEach(depInfo => {
if (depInfo.alwaysRenderConfigs.includes(property) || depInfo.config.includes(property)) {
depInfo.component.render();
}
});
},
/**
* When a player event is triggered check if any component dependencies are affected.
*/
_handleEvent: function (type) {
// Check if any components are dependent on the updated property.
componentDeps.forEach(depInfo => {
if (depInfo.alwaysRenderEvents.includes(type) || depInfo.events.includes(type)) {
depInfo.component.render();
}
});
},
/**
* Add local files.
*/
_handleFileSelect: function (e) {
e.preventDefault();
const input = e.eventTarget;
Player.playlist.addFromFiles(input.files);
},
/**
* Toggle the repeat style.
*/
_handleRepeat: function (e) {
e.preventDefault();
const values = [ 'all', 'one', 'none' ];
const current = values.indexOf(Player.config.repeat);
Player.set('repeat', values[(current + 4) % 3]);
},
/**
* Toggle the shuffle style.
*/
_handleShuffle: function (e) {
e.preventDefault();
Player.set('shuffle', !Player.config.shuffle);
Player.header.render();
// Update the play order.
if (!Player.config.shuffle) {
Player.sounds.sort((a, b) => Player.compareIds(a.id, b.id));
} else {
const sounds = Player.sounds;
for (let i = sounds.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[ sounds[i], sounds[j] ] = [ sounds[j], sounds[i] ];
}
}
Player.trigger('order');
},
_handleViewsMenu: function (e) {
e.preventDefault();
e.stopPropagation();
const dialog = _.element(Player.templates.viewsMenu());
Player.userTemplate._showMenu(e.eventTarget, dialog);
},
_showMenu: function (relative, dialog, parent) {
Player.display.closeDialogs();
parent || (parent = Player.container);
parent.appendChild(dialog);
// Position the menu.
Player.position.showRelativeTo(dialog, relative);
// Add the focused class handler
dialog.querySelectorAll('.entry').forEach(el => {
el.addEventListener('mouseenter', Player.userTemplate._setFocusedMenuItem);
});
Player.trigger('menu-open', dialog);
},
_setFocusedMenuItem: function (e) {
const submenu = e.currentTarget.querySelector('.submenu');
const menu = e.currentTarget.closest('.dialog');
const currentFocus = menu.querySelectorAll('.focused');
currentFocus.forEach(el => el.classList.remove('focused'));
e.currentTarget.classList.add('focused');
// Move the menu to the other side if there isn't room.
if (submenu && submenu.getBoundingClientRect().right > document.documentElement.clientWidth) {
submenu.style.inset = '0px 100% auto auto';
}
},
_handleFilter: function (e) {
e.preventDefault();
let filter = e.eventTarget.getAttribute('data-filter');
if (filter) {
Player.set('filters', Player.config.filters.concat(filter));
}
},
_handleDownload: function (e) {
const src = e.eventTarget.getAttribute('data-src');
const name = e.eventTarget.getAttribute('data-name') || new URL(src).pathname.split('/').pop();
GM.xmlHttpRequest({
method: 'GET',
url: src,
responseType: 'blob',
onload: response => {
const a = _.element(`<a href="${URL.createObjectURL(response.response)}" download="${name}" rel="noopener" target="_blank"></a>`);
a.click();
URL.revokeObjectURL(a.href);
},
onerror: response => Player.logError('There was an error downloading.', response, 'warning')
});
},
_handleRemove: function (e) {
const id = e.eventTarget.getAttribute('data-id');
const sound = id && Player.sounds.find(sound => sound.id === '' + id);
sound && Player.remove(sound);
},
};
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./src/config/display.js":
/*!*******************************!*\
!*** ./src/config/display.js ***!
\*******************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = [
{
property: 'autoshow',
default: true,
title: 'Autoshow',
description: 'Automatically show the player when the thread contains sounds.',
displayGroup: 'Display'
},
{
property: 'pauseOnHide',
default: true,
title: 'Pause On Hide',
description: 'Pause the player when it\'s hidden.',
displayGroup: 'Display'
},
{
property: 'showUpdatedNotification',
default: true,
title: 'Show Update Notifications',
description: 'Show notifications when the player is successfully updated.',
displayGroup: 'Display'
},
{
title: 'Controls',
displayGroup: 'Display',
settings: [
{
property: 'preventControlWrapping',
title: 'Prevent Wrapping',
description: 'Hide controls to prevent wrapping when the player is too small',
default: true
},
{
property: 'controlsHideOrder',
title: 'Hide Order',
description: 'Order controls are hidden in to prevent wrapping. Available controls are previous, next, seek-bar, time, duration, volume, mute, volume-bar, and fullscreen.',
default: [ 'fullscreen', 'duration', 'volume-bar', 'seek-bar', 'time', 'previous' ],
displayMethod: (value, attrs) => `<div class="${ns}-col"><textarea ${attrs}>${value}</textarea></div>`,
format: v => v.join('\n'),
parse: v => v.split(/\s+/)
}
]
},
{
title: 'Minimised Display',
description: 'Optional displays for when the player is minimised.',
displayGroup: 'Display',
settings: [
{
property: 'pip',
title: 'Thumbnail',
description: 'Display a fixed thumbnail of the playing sound in the bottom right of the thread.',
default: true
},
{
property: 'maxPIPWidth',
title: 'Max Width',
description: 'Maximum width for the thumbnail.',
default: '150px',
updateStylesheet: true
},
{
property: 'chanXControls',
title: '4chan X Header Controls',
description: 'Show playback controls in the 4chan X header. The display can be customised in Templates.',
displayMethod: isChanX || null,
options: {
always: 'Always',
closed: 'Only with the player closed',
never: 'Never'
}
}
]
},
{
title: 'Threads',
displayGroup: 'Display',
settings: [
{
property: 'autoScrollThread',
description: 'Automatically scroll the thread to posts as sounds play.',
title: 'Auto Scroll',
default: false
},
{
property: 'limitPostWidths',
description: 'Limit the width of posts so they aren\'t hidden under the player.',
title: 'Limit Post Widths',
default: true
},
{
property: 'minPostWidth',
title: 'Minimum Width',
default: '50%'
}
]
},
{
property: 'threadsViewStyle',
title: 'Threads View',
description: 'How threads in the threads view are listed.',
displayGroup: 'Display',
settings: [ {
title: 'Display',
default: 'table',
options: {
table: 'Table',
board: 'Board'
}
} ]
},
{
title: 'Colors',
displayGroup: 'Display',
property: 'colors',
updateStylesheet: true,
class: `${ns}-colorpicker-input`,
displayMethod: (value, attrs) => `<div class="${ns}-col">
<input type="text" ${attrs} value="${value}">
<div class="${ns}-cp-preview" style="background: ${value}"></div>
</div>`,
actions: [
{ title: 'Match Theme', handler: 'display.forceBoardTheme' }
],
// These colors will be overriden with the theme defaults at initialization.
settings: [
{
property: 'colors.text',
default: '#000000',
title: 'Text'
},
{
property: 'colors.background',
default: '#d6daf0',
title: 'Background'
},
{
property: 'colors.border',
default: '#b7c5d9',
title: 'Border'
},
{
property: 'colors.odd_row',
default: '#d6daf0',
title: 'Odd Row',
},
{
property: 'colors.even_row',
default: '#b7c5d9',
title: 'Even Row'
},
{
property: 'colors.playing',
default: '#98bff7',
title: 'Playing Row'
},
{
property: 'colors.dragging',
default: '#c396c8',
title: 'Dragging Row'
},
{
property: 'colors.controls_background',
default: '#3f3f44',
title: 'Controls Background',
description: 'The controls container element background.',
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
},
{
property: 'colors.controls_inactive',
default: '#FFFFFF',
title: 'Control Items',
description: 'The playback controls and played bar.',
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
},
{
property: 'colors.controls_active',
default: '#00b6f0',
title: 'Focused Control Items',
description: 'The control items when hovered.',
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
},
{
property: 'colors.controls_empty_bar',
default: '#131314',
title: 'Volume/Seek Bar Background',
decscription: 'The background of the volume and seek bars.',
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
},
{
property: 'colors.controls_loaded_bar',
default: '#5a5a5b',
title: 'Loaded Bar Background',
description: 'The loaded bar within the seek bar.',
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
}
]
}
];
/***/ }),
/***/ "./src/config/filter.js":
/*!******************************!*\
!*** ./src/config/filter.js ***!
\******************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = [
{
property: 'addWebm',
title: 'Include WebM',
description: 'Whether to add all WebM files regardless of a sound filename.',
default: 'soundBoards',
displayGroup: 'Filter',
options: {
always: 'Always',
soundBoards: 'Boards with sound',
never: 'Never'
}
},
{
property: 'allow',
title: 'Allowed Hosts',
description: 'Which domains sources are allowed to be loaded from.',
default: [
'4cdn.org',
'catbox.moe',
'dmca.gripe',
'lewd.se',
'pomf.cat',
'zz.ht'
],
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
displayGroup: 'Filter',
split: '\n'
},
{
property: 'filters',
default: [ '# Image MD5 or sound URL' ],
title: 'Filters',
description: 'List of URLs or image MD5s to filter, one per line.\nLines starting with a # will be ignored.',
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
displayGroup: 'Filter',
split: '\n'
}
];
/***/ }),
/***/ "./src/config/hosts.js":
/*!*****************************!*\
!*** ./src/config/hosts.js ***!
\*****************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = [
{
property: 'defaultUploadHost',
default: 'catbox',
parse: 'settings.setDefaultHost'
},
{
property: 'uploadHosts',
title: 'Hosts',
actions: [
{ title: 'Add', handler: 'settings.addUploadHost' },
{ title: 'Restore Defaults', handler: 'settings.restoreDefaultHosts' },
],
displayGroup: 'Hosts',
displayMethod: 'settings.renderHosts',
parse: 'settings.parseHosts',
looseCompare: true,
dismissTextId: 'uplodHostSettings',
dismissRestoreText: 'Show Help',
text: 'Properties'
+ '<br><strong>Name</strong>: A unique identifier.'
+ '<br><strong>URL</strong>: The URL to post the file to.'
+ '<br><strong>Response Path/Match</strong>: A key path or regular expression to locate the uploaded filename in the response.'
+ '<br><strong>File URL Format</strong>: The URL format for uploaded sounds. %s is replaced with the result of response path/match if given or the full response.'
+ '<br><strong>Data</strong>: The form data for the upload (as JSON). Specify the file using $file.',
mix: true,
default: {
catbox: {
default: true,
url: 'https://catbox.moe/user/api.php',
data: { reqtype: 'fileupload', fileToUpload: '$file', userhash: null },
filenameLength: 29
},
pomf: {
url: 'https://pomf.cat/upload.php',
data: { 'files[]': '$file' },
responsePath: 'files.0.url',
filenameLength: 23
},
zz: {
url: 'https://zz.ht/api/upload',
responsePath: 'files.0.url',
data: {
'files[]': '$file'
},
headers: {
token: null
},
filenameLength: 19
},
lewd: {
url: 'https://lewd.se/upload',
data: { file: '$file' },
headers: { token: null, shortUrl: true },
responsePath: 'data.link',
filenameLength: 30
}
}
}
];
/***/ }),
/***/ "./src/config/index.js":
/*!*****************************!*\
!*** ./src/config/index.js ***!
\*****************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
module.exports = [
// Order the groups appear in.
...__webpack_require__(/*! ./display */ "./src/config/display.js"),
...__webpack_require__(/*! ./filter */ "./src/config/filter.js"),
...__webpack_require__(/*! ./keybinds */ "./src/config/keybinds.js"),
...__webpack_require__(/*! ./templates */ "./src/config/templates.js"),
...__webpack_require__(/*! ./hosts */ "./src/config/hosts.js"),
{
property: 'shuffle',
default: false
},
{
property: 'repeat',
default: 'all'
},
{
property: 'viewStyle',
default: 'playlist'
},
{
property: 'hoverImages',
default: false
},
{
property: 'showPlaylistSearch',
default: true
}
];
/***/ }),
/***/ "./src/config/keybinds.js":
/*!********************************!*\
!*** ./src/config/keybinds.js ***!
\********************************/
/*! no static exports found */
/***/ (function(module, exports) {
const hasMediaSession = 'mediaSession' in navigator;
module.exports = [
{
property: 'hardwareMediaKeys',
title: 'Hardware Media Keys',
displayGroup: 'Keybinds',
description: 'Enable playback control via hardware media keys.'
+ (!hasMediaSession ? ' Your browser does not support this feature.' : ''),
default: hasMediaSession,
attrs: !hasMediaSession && 'disabled'
},
{
title: 'Keybinds',
displayGroup: 'Keybinds',
description: 'Enable keyboard shortcuts.',
format: 'hotkeys.stringifyKey',
parse: 'hotkeys.parseKey',
class: `${ns}-key-input`,
property: 'hotkey_bindings',
settings: [
{
property: 'hotkeys',
default: 'open',
title: 'Enabled',
format: null,
parse: null,
class: null,
options: {
always: 'Always',
open: 'Only with the player open',
never: 'Never'
}
},
{
property: 'hotkey_bindings.playPause',
title: 'Play/Pause',
keyHandler: 'togglePlay',
ignoreRepeat: true,
default: { key: ' ' }
},
{
property: 'hotkey_bindings.previous',
title: 'Previous',
keyHandler: () => Player.previous({ force: true }),
ignoreRepeat: true,
default: { key: 'arrowleft' }
},
{
property: 'hotkey_bindings.next',
title: 'Next',
keyHandler: () => Player.next({ force: true }),
ignoreRepeat: true,
default: { key: 'arrowright' }
},
{
property: 'hotkey_bindings.previousGroup',
title: 'Previous Group',
keyHandler: () => Player.previous({ force: true, group: true }),
ignoreRepeat: true,
default: { shiftKey: true, key: 'arrowleft' }
},
{
property: 'hotkey_bindings.nextGroup',
title: 'Next Group',
keyHandler: () => Player.next({ force: true, group: true }),
ignoreRepeat: true,
default: { shiftKey: true, key: 'arrowright' }
},
{
property: 'hotkey_bindings.volumeUp',
title: 'Volume Up',
keyHandler: 'controls.volumeUp',
default: { shiftKey: true, key: 'arrowup' }
},
{
property: 'hotkey_bindings.volumeDown',
title: 'Volume Down',
keyHandler: 'controls.volumeDown',
default: { shiftKey: true, key: 'arrowdown' }
},
{
property: 'hotkey_bindings.closePlayer',
title: 'Close',
keyHandler: 'display.close',
default: { key: '' }
},
{
property: 'hotkey_bindings.togglePlayer',
title: 'Show/Hide',
keyHandler: 'display.toggle',
default: { key: 'h' }
},
{
property: 'hotkey_bindings.toggleFullscreen',
title: 'Toggle Fullscreen',
keyHandler: 'display.toggleFullScreen',
default: { key: '' }
},
{
property: 'hotkey_bindings.togglePlaylist',
title: 'Toggle Playlist',
keyHandler: 'playlist.toggleView',
default: { key: '' }
},
{
property: 'hotkey_bindings.toggleSearch',
title: 'Toggle Playlist Search',
keyHandler: () => Player.set('showPlaylistSearch', !Player.config.showPlaylistSearch),
default: { key: '' }
},
{
property: 'hotkey_bindings.scrollToPlaying',
title: 'Jump To Playing',
keyHandler: 'playlist.scrollToPlaying',
default: { key: '' }
},
{
property: 'hotkey_bindings.toggleHoverImages',
title: 'Toggle Hover Images',
keyHandler: 'playlist.toggleHoverImages',
default: { key: '' }
},
{
property: 'hotkey_bindings.toggleAutoScroll',
title: 'Toggle Auto Scroll',
keyHandler: () => Player.set('autoScrollThread', !Player.config.autoScrollThread),
default: { key: '' }
}
]
}
];
/***/ }),
/***/ "./src/config/templates.js":
/*!*********************************!*\
!*** ./src/config/templates.js ***!
\*********************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = [
{
property: 'headerTemplate',
title: 'Header',
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
default: 'repeat-button shuffle-button hover-images-button playlist-button\nsound-name-marquee\nview-menu-button add-button reload-button close-button',
displayGroup: 'Templates',
displayMethod: 'textarea'
},
{
property: 'rowTemplate',
title: 'Row',
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
default: 'sound-name h:{menu-button}',
displayGroup: 'Templates',
displayMethod: 'textarea'
},
{
property: 'footerTemplate',
title: 'Footer',
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
default: 'playing-button:"sound-index /" sound-count sounds\n'
+ 'p:{\n'
+ ' <div style="float: right; margin-right: .5rem">\n'
+ ' post-link\n'
+ ' Open [ image-link sound-link ]\n'
+ ' Download [ dl-image-button dl-sound-button ]\n'
+ ' </div>\n'
+ '}',
description: 'Template for the footer contents',
displayGroup: 'Templates',
displayMethod: 'textarea',
attrs: 'style="height:9em;"'
},
{
property: 'chanXTemplate',
title: '4chan X Header',
default: 'p:{\n\tpost-link:"sound-name"\n\tprev-button\n\tplay-button\n\tnext-button\n\tsound-current-time / sound-duration\n}',
actions: [ { title: 'Reset', handler: 'settings.reset' } ],
displayGroup: 'Templates',
displayMethod: 'textarea',
attrs: 'style="height:9em;"'
}
];
/***/ }),
/***/ "./src/file_parser.js":
/*!****************************!*\
!*** ./src/file_parser.js ***!
\****************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {const selectors = __webpack_require__(/*! ./selectors */ "./src/selectors.js");
const protocolRE = /^(https?:)?\/\//;
const filenameRE = /(.*?)[[({](?:audio|sound)[ =:|$](.*?)[\])}]/gi;
let localCounter = 0;
module.exports = {
parseFiles,
parsePost,
parseFileName
};
function parseFiles(target, postRender) {
let addedSounds = false;
let posts = target.classList.contains('post')
? [ target ]
: target.querySelectorAll(selectors.posts);
posts.forEach(post => parsePost(post, postRender) && (addedSounds = true));
if (addedSounds && postRender && Player.container) {
Player.playlist.render();
}
}
function parsePost(post, skipRender) {
try {
if (post.classList.contains('style-fetcher')) {
return;
}
const parentParent = post.parentElement.parentElement;
if (parentParent.id === 'qp' || post.parentElement.classList.contains('noFile')) {
return;
}
// If there's a play button this post has already been parsed. Just wire up the link.
let playLink = post.querySelector(`.${ns}-play-link`);
if (playLink) {
const id = playLink.getAttribute('data-id');
playLink.onclick = () => Player.play(Player.sounds.find(sound => sound.id === id));
return;
}
let filename = null;
let filenameLocations = selectors.filename;
Object.keys(filenameLocations).some(function (selector) {
const node = post.querySelector(selector);
return node && (filename = node[filenameLocations[selector]]);
});
if (!filename) {
return;
}
selectors.filenameParser && (filename = selectors.filenameParser(filename));
const postID = post.id.slice(selectors.postIdPrefix.length);
const fileThumb = post.querySelector(selectors.thumb).closest('a');
const imageSrc = fileThumb && fileThumb.href;
const thumbImg = fileThumb && fileThumb.querySelector('img');
const thumbSrc = thumbImg && thumbImg.src;
const imageMD5 = Site === 'Fuuka'
? post.querySelector(':scope > a:nth-of-type(3)').href.split('/').pop()
: thumbImg && thumbImg.getAttribute('data-md5');
const sounds = parseFileName(filename, imageSrc, postID, thumbSrc, imageMD5);
if (!sounds.length) {
return;
}
// Create a play link
const firstID = sounds[0].id;
const linkInfo = selectors.playLink;
const content = `<a href="javascript:;" class="${linkInfo.class}" data-id="${firstID}">${linkInfo.text}</a>`;
const playLinkRelative = linkInfo.relative && post.querySelector(linkInfo.relative);
linkInfo.prependText && _addPlayLinkText(linkInfo.prependText, linkInfo.before, playLinkRelative);
playLink = linkInfo.before
? _.elementBefore(content, playLinkRelative)
: _.element(content, playLinkRelative);
linkInfo.appendText && _addPlayLinkText(linkInfo.appendText, linkInfo.before, playLinkRelative);
playLink.onclick = () => Player.play(sounds[0]);
// Don't add sounds from inline quotes of posts in the thread
sounds.forEach(sound => Player.add(sound, skipRender));
return sounds.length > 0;
} catch (err) {
Player.logError('There was an issue parsing the files. Please check the console for details.', err);
console.log('[4chan sounds player]', post);
}
}
function _addPlayLinkText(text, before, relative) {
const node = text && document.createTextNode(text);
if (before) {
relative.parentNode.insertBefore(node, relative);
} else {
relative.appendChild(node);
}
}
function parseFileName(filename, image, post, thumb, imageMD5, bypassVerification) {
if (!filename) {
return [];
}
filename = filename.replace(/-/, '/');
const matches = [];
let match;
while ((match = filenameRE.exec(filename)) !== null) {
matches.push(match);
}
// Add webms without a sound filename as a standable video if enabled
if (!matches.length && (Player.config.addWebm === 'always' || (Player.config.addWebm === 'soundBoards' && (Board === 'gif' || Board === 'wsg'))) && filename.endsWith('.webm')) {
matches.push([ null, filename.slice(0, -5), image ]);
}
const defaultName = matches[0] && matches[0][1] || post || 'Local Sound ' + localCounter;
matches.length && !post && localCounter++;
return matches.reduce((sounds, match, i) => {
let src = match[2];
const id = (post || 'local' + localCounter) + ':' + i;
const name = match[1].trim();
const title = name || defaultName + (matches.length > 1 ? ` (${i + 1})` : '');
const standaloneVideo = src === image;
try {
if (src.includes('%')) {
src = decodeURIComponent(src);
}
if (!src.startsWith('blob:') && src.match(protocolRE) === null) {
src = (location.protocol + '//' + src);
}
} catch (error) {
return sounds;
}
const sound = { src, id, title, name, post, image, filename, thumb, imageMD5, standaloneVideo };
if (bypassVerification || Player.acceptedSound(sound)) {
sounds.push(sound);
}
return sounds;
}, []);
}
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./src/globals.js":
/*!************************!*\
!*** ./src/globals.js ***!
\************************/
/*! no static exports found */
/***/ (function(module, exports) {
/**
* Global variables and helpers.
*/
window.ns = 'fc-sounds';
window.is4chan = location.hostname.includes('4chan.org') || location.hostname.includes('4channel.org');
window.isChanX = document.documentElement && document.documentElement.classList.contains('fourchan-x');
window.Board = location.pathname.split('/')[1];
// Determine what type of site this is. Default to FoolFuuka as the most common archiver.
window.Site = is4chan ? '4chan'
: ((document.head.querySelector('meta[name="generator"]') || {}).content || '').includes('FoolFuuka') ? 'FoolFuuka'
: ((document.head.querySelector('meta[name="description"]') || {}).content || '').includes('Fuuka') ? 'Fuuka'
: 'FoolFuuka';
class PlayerError extends Error {
constructor(msg, type, err) {
super(msg);
this.reason = msg;
this.type = type;
this.error = err;
}
}
window.PlayerError = PlayerError;
/***/ }),
/***/ "./src/icons.js":
/*!**********************!*\
!*** ./src/icons.js ***!
\**********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
module.exports = {
fcSounds: '<svg width="452" height="257" xmlns="http://www.w3.org/2000/svg"><g><text font-weight="bold" font-style="italic" font-family="Helvetica, Arial, sans-serif" font-size="250" y="197" fill-opacity="0.05" fill="#000000">4sp</text></g></svg>',
arrowClockwise: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/arrow-clockwise.svg */ "./node_modules/bootstrap-icons/icons/arrow-clockwise.svg").default,
arrowsCollapse: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/arrows-collapse.svg */ "./node_modules/bootstrap-icons/icons/arrows-collapse.svg").default,
arrowsExpand: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/arrows-expand.svg */ "./node_modules/bootstrap-icons/icons/arrows-expand.svg").default,
arrowRepeat: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/arrow-repeat.svg */ "./node_modules/bootstrap-icons/icons/arrow-repeat.svg").default,
chatRightQuote: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/chat-right-quote.svg */ "./node_modules/bootstrap-icons/icons/chat-right-quote.svg").default,
chevronDown: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/chevron-down.svg */ "./node_modules/bootstrap-icons/icons/chevron-down.svg").default,
close: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/plus.svg */ "./node_modules/bootstrap-icons/icons/plus.svg").default.replace('bi-plus', `bi-plus ${ns}-icon-close`).replace(/viewBox="[^"]+"/, 'viewBox="2 2 12 12"'),
gear: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/gear.svg */ "./node_modules/bootstrap-icons/icons/gear.svg").default,
fileEarmarkImage: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/file-earmark-image.svg */ "./node_modules/bootstrap-icons/icons/file-earmark-image.svg").default,
fileEarmarkMusic: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/file-earmark-music.svg */ "./node_modules/bootstrap-icons/icons/file-earmark-music.svg").default,
filter: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/filter.svg */ "./node_modules/bootstrap-icons/icons/filter.svg").default,
fullscreen: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/fullscreen.svg */ "./node_modules/bootstrap-icons/icons/fullscreen.svg").default,
fullscreenExit: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/fullscreen-exit.svg */ "./node_modules/bootstrap-icons/icons/fullscreen-exit.svg").default,
image: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/image.svg */ "./node_modules/bootstrap-icons/icons/image.svg").default,
link: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/link-45deg.svg */ "./node_modules/bootstrap-icons/icons/link-45deg.svg").default,
musicNoteList: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/music-note-list.svg */ "./node_modules/bootstrap-icons/icons/music-note-list.svg").default,
play: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/play.svg */ "./node_modules/bootstrap-icons/icons/play.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="2 2 12 12"'),
playFill: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/play-fill.svg */ "./node_modules/bootstrap-icons/icons/play-fill.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="2 2 12 12"'),
pause: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/pause.svg */ "./node_modules/bootstrap-icons/icons/pause.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="2 2 12 12"'),
pauseFill: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/pause-fill.svg */ "./node_modules/bootstrap-icons/icons/pause-fill.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="2 2 12 12"'),
plus: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/plus-circle.svg */ "./node_modules/bootstrap-icons/icons/plus-circle.svg").default,
reboot: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/bootstrap-reboot.svg */ "./node_modules/bootstrap-icons/icons/bootstrap-reboot.svg").default,
search: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/search.svg */ "./node_modules/bootstrap-icons/icons/search.svg").default,
shuffle: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/shuffle.svg */ "./node_modules/bootstrap-icons/icons/shuffle.svg").default,
skipEnd: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/skip-end.svg */ "./node_modules/bootstrap-icons/icons/skip-end.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="2 2 12 12"'),
skipEndFill: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/skip-end-fill.svg */ "./node_modules/bootstrap-icons/icons/skip-end-fill.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="2 2 12 12"'),
skipStart: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/skip-start.svg */ "./node_modules/bootstrap-icons/icons/skip-start.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="2 2 12 12"'),
skipStartFill: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/skip-start-fill.svg */ "./node_modules/bootstrap-icons/icons/skip-start-fill.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="2 2 12 12"'),
soundwave: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/soundwave.svg */ "./node_modules/bootstrap-icons/icons/soundwave.svg").default,
tools: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/tools.svg */ "./node_modules/bootstrap-icons/icons/tools.svg").default,
trash: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/trash.svg */ "./node_modules/bootstrap-icons/icons/trash.svg").default,
volumeMute: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/volume-mute.svg */ "./node_modules/bootstrap-icons/icons/volume-mute.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="1 1 14 14"'),
volumeMuteFill: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/volume-mute-fill.svg */ "./node_modules/bootstrap-icons/icons/volume-mute-fill.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="1 1 14 14"'),
volumeUp: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/volume-up.svg */ "./node_modules/bootstrap-icons/icons/volume-up.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="1 1 14 14"'),
volumeUpFill: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/volume-up-fill.svg */ "./node_modules/bootstrap-icons/icons/volume-up-fill.svg").default.replace(/viewBox="[^"]+"/, 'viewBox="1 1 14 14"')
};
/***/ }),
/***/ "./src/main.js":
/*!*********************!*\
!*** ./src/main.js ***!
\*********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
async function doInit() {
// Require globals again here just in case 4chan X loaded before timeout below.
__webpack_require__(/*! ./globals */ "./src/globals.js");
// Require these here so every other require is sure of the 4chan X state.
const Player = __webpack_require__(/*! ./player */ "./src/player.js");
const { parseFiles } = __webpack_require__(/*! ./file_parser */ "./src/file_parser.js");
await Player.initialize();
parseFiles(document.body, true);
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', doInit);
// The timeout makes sure 4chan X will have added it's classes and be identified.
// The player also tends to be all black without a timeout.
// Something with the timing of the stylesheet loading and applying the board theme.
setTimeout(function () {
__webpack_require__(/*! ./globals */ "./src/globals.js");
// If it's already known 4chan X is installed this can be skipped.
if (!isChanX) {
if (document.readyState !== 'loading') {
doInit();
} else {
document.addEventListener('DOMContentLoaded', doInit);
}
}
}, 0);
/***/ }),
/***/ "./src/migrations.js":
/*!***************************!*\
!*** ./src/migrations.js ***!
\***************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = [
{
version: '3.0.0',
name: 'hosts-filename-length',
async run() {
const defaultHosts = Player.settings.findDefault('uploadHosts').default;
Object.keys(defaultHosts).forEach(host => {
Player.config.uploadHosts[host].filenameLength = defaultHosts[host].filenameLength;
});
}
}
];
/***/ }),
/***/ "./src/player.js":
/*!***********************!*\
!*** ./src/player.js ***!
\***********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {const components = {
// Settings must be first.
settings: __webpack_require__(/*! ./components/settings */ "./src/components/settings.js"),
events: __webpack_require__(/*! ./components/events */ "./src/components/events.js"),
actions: __webpack_require__(/*! ./components/actions */ "./src/components/actions.js"),
colorpicker: __webpack_require__(/*! ./components/colorpicker */ "./src/components/colorpicker.js"),
controls: __webpack_require__(/*! ./components/controls */ "./src/components/controls.js"),
display: __webpack_require__(/*! ./components/display */ "./src/components/display.js"),
footer: __webpack_require__(/*! ./components/footer */ "./src/components/footer.js"),
header: __webpack_require__(/*! ./components/header */ "./src/components/header.js"),
hotkeys: __webpack_require__(/*! ./components/hotkeys */ "./src/components/hotkeys.js"),
minimised: __webpack_require__(/*! ./components/minimised */ "./src/components/minimised.js"),
playlist: __webpack_require__(/*! ./components/playlist */ "./src/components/playlist.js"),
position: __webpack_require__(/*! ./components/position */ "./src/components/position.js"),
threads: __webpack_require__(/*! ./components/threads */ "./src/components/threads.js"),
tools: __webpack_require__(/*! ./components/tools */ "./src/components/tools.js"),
userTemplate: __webpack_require__(/*! ./components/user-template */ "./src/components/user-template/index.js")
};
// Create a global ref to the player.
const Player = window.Player = module.exports = {
ns,
audio: new Audio(),
sounds: [],
isHidden: true,
container: null,
ui: {},
_public: [],
// Build the config from the default
config: {},
// Helper function to query elements in the player.
$: (...args) => Player.container && Player.container.querySelector(...args),
$all: (...args) => Player.container && Player.container.querySelectorAll(...args),
// Store a ref to the components so they can be iterated.
components,
// Get all the templates.
templates: {
body: __webpack_require__(/*! ./templates/body.tpl */ "./src/templates/body.tpl"),
colorpicker: __webpack_require__(/*! ./templates/colorpicker.tpl */ "./src/templates/colorpicker.tpl"),
controls: __webpack_require__(/*! ./templates/controls.tpl */ "./src/templates/controls.tpl"),
css: __webpack_require__(/*! ./scss/style.scss */ "./src/scss/style.scss"),
css4chanXPolyfill: __webpack_require__(/*! ./scss/4chan-x-polyfill.scss */ "./src/scss/4chan-x-polyfill.scss"),
footer: __webpack_require__(/*! ./templates/footer.tpl */ "./src/templates/footer.tpl"),
header: __webpack_require__(/*! ./templates/header.tpl */ "./src/templates/header.tpl"),
hostInput: __webpack_require__(/*! ./templates/host_input.tpl */ "./src/templates/host_input.tpl"),
itemMenu: __webpack_require__(/*! ./templates/item_menu.tpl */ "./src/templates/item_menu.tpl"),
list: __webpack_require__(/*! ./templates/list.tpl */ "./src/templates/list.tpl"),
player: __webpack_require__(/*! ./templates/player.tpl */ "./src/templates/player.tpl"),
settings: __webpack_require__(/*! ./templates/settings.tpl */ "./src/templates/settings.tpl"),
threads: __webpack_require__(/*! ./templates/threads.tpl */ "./src/templates/threads.tpl"),
threadBoards: __webpack_require__(/*! ./templates/thread_boards.tpl */ "./src/templates/thread_boards.tpl"),
threadList: __webpack_require__(/*! ./templates/thread_list.tpl */ "./src/templates/thread_list.tpl"),
tools: __webpack_require__(/*! ./templates/tools.tpl */ "./src/templates/tools.tpl"),
viewsMenu: __webpack_require__(/*! ./templates/views_menu.tpl */ "./src/templates/views_menu.tpl")
},
/**
* Set up the player.
*/
initialize: async function initialize() {
if (Player.initialized) {
return;
}
Player.initialized = true;
try {
Player.sounds = [ ];
// Run the initialisation for each component.
for (let name in components) {
components[name].initialize && await components[name].initialize();
}
// Show a button to open the player.
Player.display.createPlayerButton();
// Render the player, but not neccessarily show it.
Player.display.render();
// Expose some functionality via PlayerEvent custom events.
document.addEventListener('PlayerEvent', e => {
if (e.detail.action && ( true || false)) {
return _.get(Player, e.detail.action).apply(window, e.detail.arguments);
}
});
} catch (err) {
Player.logError('There was an error initialzing the sound player. Please check the console for details.', err);
// Can't recover so throw this error.
throw err;
}
},
/**
* 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;
},
/**
* Compare two ids for sorting.
*/
compareIds: function (a, b) {
const [ aPID, aSID ] = a.split(':');
const [ bPID, bSID ] = b.split(':');
const postDiff = aPID - bPID;
return postDiff !== 0 ? postDiff : aSID - bSID;
},
/**
* Check whether a sound src and image are allowed and not filtered.
*/
acceptedSound: function ({ src, imageMD5 }) {
try {
const link = new URL(src);
const host = link.hostname.toLowerCase();
return !Player.config.filters.find(v => v === imageMD5 || v === host + link.pathname)
&& Player.config.allow.find(h => host === h || host.endsWith('.' + h));
} catch (err) {
return false;
}
},
/**
* Listen for changes
*/
syncTab: (property, callback) => typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener(property, (_prop, oldValue, newValue, remote) => {
remote && callback(newValue, oldValue);
}),
/**
* Log errors and show an error notification.
*/
logError: function (message, error, type) {
console.error('[4chan sounds player]', message, error);
if (error instanceof PlayerError) {
error.error && console.error('[4chan sound player]', error.error);
message = error.reason;
type = error.type || type;
}
Player.alert(message, type || 'error', 5);
},
/**
* Show a notification using 4chan X or the native extention.
*/
alert: function (content, type = 'info', lifetime = 5) {
if (isChanX) {
content = _.element(`<span>${content}</span`);
document.dispatchEvent(new CustomEvent('CreateNotification', {
bubbles: true,
detail: { content, type, lifetime }
}));
} else if (typeof Feedback !== 'undefined') {
Feedback.showMessage(content, type === 'info' ? 'notify' : 'error', lifetime * 1000);
}
}
};
// Add each of the components to the player.
for (let name in components) {
Player[name] = components[name];
(Player[name].atRoot || []).forEach(k => Player[k] = Player[name][k]);
(Player[name].public || []).forEach(k => {
Player._public.push((Player[name].atRoot || []).includes(k) ? k : `${name}.${k}`);
});
}
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./src/scss/4chan-x-polyfill.scss":
/*!****************************************!*\
!*** ./src/scss/4chan-x-polyfill.scss ***!
\****************************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = (data = {}) => `.dialog {
background: ${Player.config.colors.background};
background: ${Player.config.colors.background};
border-color: ${Player.config.colors.border};
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
border-radius: 3px;
padding-top: 1px;
padding-bottom: 3px;
}
.entry {
position: relative;
display: block;
padding: 0.125rem 0.5rem;
min-width: 70px;
white-space: nowrap;
}
.entry.has-submenu::after {
content: "";
border-left: 0.5em solid;
border-top: 0.3em solid transparent;
border-bottom: 0.3em solid transparent;
display: inline-block;
margin: 0.35em;
position: absolute;
right: 3px;
}
.entry.focused {
background: rgba(255, 255, 255, 0.33);
}
.entry.focused > .submenu {
display: block;
}
.submenu {
position: absolute;
display: none;
}`
/***/ }),
/***/ "./src/scss/style.scss":
/*!*****************************!*\
!*** ./src/scss/style.scss ***!
\*****************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = (data = {}) => `.${ns}-colorpicker {
position: fixed;
padding: 0.25rem;
white-space: nowrap;
z-index: 999;
}
.${ns}-colorpicker .${ns}-cp-saturation {
display: inline-block;
position: relative;
}
.${ns}-colorpicker .${ns}-cp-saturation .position {
width: 5px;
}
.${ns}-colorpicker .${ns}-cp-saturation::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: inherit;
background: black;
-webkit-mask-image: linear-gradient(#0000, #000);
mask-image: linear-gradient(#0000, #000);
}
.${ns}-colorpicker .${ns}-cp-hue {
margin-left: 0.5rem;
display: inline-block;
position: relative;
width: 30px;
background: linear-gradient(to bottom, #F00, #FF0, #0F0, #0FF, #00F, #F0F, #F00);
}
.${ns}-colorpicker .${ns}-cp-hue .position {
top: -3px;
left: -1px;
right: -1px;
}
.${ns}-colorpicker .${ns}-cp-saturation .position, .${ns}-colorpicker .${ns}-cp-hue .position {
position: absolute;
height: 5px;
border-radius: 1rem;
}
.${ns}-colorpicker .${ns}-output {
vertical-align: top;
margin-left: 1rem;
display: inline-block;
}
.${ns}-colorpicker .${ns}-output .${ns}-rgb-input {
width: 2rem;
}
.${ns}-colorpicker .${ns}-output .output-color {
height: 40px;
margin: 0.25rem 0;
}
.${ns}-cp-preview, .${ns}-cp-saturation .position, .${ns}-cp-hue .position, .${ns}-output .output-color {
border: solid 1px black;
box-shadow: inset 0 0 0 1px #EEE;
}
.${ns}-cp-preview {
height: 1em;
width: 1em;
margin-left: 0.125rem;
}
.${ns}-text-muted {
color: #909090;
}
.${ns}-controls {
align-items: center;
padding: 0.5rem 0;
position: relative;
background: ${Player.config.colors.controls_background};
justify-content: space-between;
}
.${ns}-controls > div {
margin: 0 0.5rem;
}
.${ns}-controls .${ns}-current-time {
color: ${Player.config.colors.controls_inactive};
}
.${ns}-controls .${ns}-media-control {
width: 1.5rem;
height: 1.5rem;
font-size: 1rem;
color: ${Player.config.colors.controls_inactive};
}
.${ns}-controls .${ns}-media-control:hover {
color: ${Player.config.colors.controls_active};
}
.${ns}-media-control {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.${ns}-media-control.${ns}-hover-fill svg[class$=-fill], .${ns}-media-control.${ns}-hover-fill svg[class*="-fill "] {
display: none;
}
.${ns}-media-control.${ns}-hover-fill:hover svg {
display: none;
}
.${ns}-media-control.${ns}-hover-fill:hover svg[class$=-fill], .${ns}-media-control.${ns}-hover-fill:hover svg[class*="-fill "] {
display: block;
}
.${ns}-media-control.${ns}-play-button:not(.${ns}-play) svg.bi-play-fill, .${ns}-media-control.${ns}-play-button:not(.${ns}-play) svg.bi-play {
display: none !important;
}
.${ns}-media-control.${ns}-play-button.${ns}-play svg.bi-pause-fill, .${ns}-media-control.${ns}-play-button.${ns}-play svg.bi-pause {
display: none !important;
}
.${ns}-media-control.${ns}-fullscreen-button svg.bi-fullscreen-exit {
display: none;
}
#${ns}-container[data-view-style=fullscreen] .${ns}-media-control.${ns}-fullscreen-button svg.bi-fullscreen-exit {
display: block;
}
#${ns}-container[data-view-style=fullscreen] .${ns}-media-control.${ns}-fullscreen-button svg.bi-fullscreen {
display: none;
}
.${ns}-media-control.${ns}-volume-button.mute .bi:not(.bi-volume-mute):not(.bi-volume-mute-fill) {
display: none;
}
.${ns}-media-control.${ns}-volume-button.up .bi:not(.bi-volume-up):not(.bi-volume-up-fill) {
display: none;
}
.${ns}-progress-bar {
min-width: 3.5rem;
height: 1.5rem;
display: flex;
align-items: center;
}
.${ns}-progress-bar .${ns}-full-bar {
height: 0.3rem;
width: 100%;
background: ${Player.config.colors.controls_empty_bar};
border-radius: 1rem;
position: relative;
}
.${ns}-progress-bar .${ns}-full-bar > div {
position: absolute;
top: 0;
bottom: 0;
border-radius: 1rem;
}
.${ns}-progress-bar .${ns}-full-bar .${ns}-loaded-bar {
background: ${Player.config.colors.controls_loaded_bar};
}
.${ns}-progress-bar .${ns}-full-bar .${ns}-current-bar {
display: flex;
justify-content: flex-end;
align-items: center;
}
.${ns}-progress-bar .${ns}-full-bar .${ns}-current-bar:after {
content: "";
background: ${Player.config.colors.controls_inactive};
height: 0.8rem;
min-width: 0.8rem;
border-radius: 1rem;
box-shadow: rgba(0, 0, 0, 0.76) 0 0 3px 0;
margin-right: -0.4rem;
}
.${ns}-progress-bar:hover .${ns}-current-bar:after {
background: ${Player.config.colors.controls_active};
}
.${ns}-seek-bar .${ns}-current-bar {
background: ${Player.config.colors.controls_active};
}
.${ns}-volume-bar .${ns}-current-bar {
background: ${Player.config.colors.controls_inactive};
}
.${ns}-chan-x-controls .${ns}-current-time, .${ns}-chan-x-controls .${ns}-duration {
margin: 0 0.25rem;
}
.${ns}-footer {
padding: 0.15rem 0.25rem;
border-top: solid 1px ${Player.config.colors.border};
}
.${ns}-footer .${ns}-expander {
position: absolute;
bottom: 0px;
right: 0px;
height: 0.75rem;
width: 0.75rem;
cursor: se-resize;
background: linear-gradient(to bottom right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 50%, ${Player.config.colors.border} 55%, ${Player.config.colors.border} 100%);
}
.${ns}-footer:hover .${ns}-hover-display {
display: inline-block;
}
.${ns}-header {
cursor: grab;
text-align: center;
border-bottom: solid 1px ${Player.config.colors.border};
padding: 0.125rem;
}
.${ns}-header:hover .${ns}-hover-display {
display: flex;
}
.${ns}-title-marquee {
transition: margin-left 1s linear;
}
.${ns}-icon-close {
transform: rotate(45deg);
}
.${ns}-footer svg.bi {
margin-bottom: -0.2rem;
}
.${ns}-menu svg.bi {
margin: 0 -0.25rem 0 0.25rem;
}
.${ns}-header svg.bi {
margin: 0 0.125rem;
}
.muted svg.bi {
opacity: 0.45;
}
svg.bi {
overflow: visible;
}
.${ns}-image-link {
text-align: center;
display: flex;
justify-items: center;
justify-content: center;
}
.${ns}-image-link.${ns}-pip {
position: fixed;
right: 10px;
height: ${Player.config.maxPIPWidth} !important;
max-width: ${Player.config.maxPIPWidth};
align-items: end;
}
.${ns}-image-link.${ns}-pip .${ns}-image, .${ns}-image-link.${ns}-pip .${ns}-video {
max-height: 100%;
height: initial;
width: initial;
object-fit: contain;
}
.${ns}-image-link .${ns}-video {
display: none;
}
.${ns}-image, .${ns}-video {
height: 100%;
width: 100%;
object-fit: contain;
}
.${ns}-image-link.${ns}-show-video .${ns}-video {
display: block;
}
.${ns}-image-link.${ns}-show-video .${ns}-image {
display: none;
}
#${ns}-container {
position: fixed;
background: ${Player.config.colors.background};
border: 1px solid ${Player.config.colors.border};
min-width: 7rem;
color: ${Player.config.colors.text};
}
.${ns}-panel {
padding: 0 0.25rem;
height: 100%;
width: calc(100% - .5rem);
overflow: auto;
}
.${ns}-heading {
font-weight: 600;
margin: 0.5rem 0;
}
.${ns}-has-description {
cursor: help;
}
.${ns}-heading-action {
font-weight: normal;
text-decoration: underline;
margin-left: 0.25rem;
}
.${ns}-row {
display: flex;
flex-wrap: wrap;
min-width: 100%;
box-sizing: border-box;
}
.${ns}-row.nowrap {
flex-wrap: nowrap;
}
.${ns}-col-auto {
flex: 0 0 auto;
width: auto;
max-width: 100%;
display: inline-flex;
}
.${ns}-col {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
width: 100%;
position: relative;
}
.${ns}-align-center {
align-items: center;
align-content: center;
align-self: center;
}
.${ns}-align-start {
align-items: start;
align-content: start;
align-self: start;
}
.${ns}-space-between {
justify-content: space-between;
}
html.fourchan-x #${ns}-container .fa {
font-size: 0;
visibility: hidden;
margin: 0 0.15rem;
}
.${ns}-truncate-text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.${ns}-hover-display {
display: none;
}
.${ns}-player .${ns}-hover-image {
position: fixed;
max-height: 125px;
max-width: 125px;
}
.${ns}-player.${ns}-hide-hover-image .${ns}-hover-image {
display: none !important;
}
.${ns}-list-container {
overflow-y: auto;
}
.${ns}-list-container .${ns}-list-item {
list-style-type: none;
padding: 0.15rem 0.25rem;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
background: ${Player.config.colors.odd_row};
overflow: hidden;
height: 1.3rem;
}
.${ns}-list-container .${ns}-list-item.playing {
background: ${Player.config.colors.playing} !important;
}
.${ns}-list-container .${ns}-list-item:nth-child(2n) {
background: ${Player.config.colors.even_row};
}
.${ns}-list-container .${ns}-list-item .${ns}-item-menu-button {
right: 0.25rem;
}
.${ns}-list-container .${ns}-list-item:hover .${ns}-hover-display {
display: flex;
}
.${ns}-list-container .${ns}-list-item.${ns}-dragging {
background: ${Player.config.colors.dragging};
}
.${ns}-settings textarea {
border: solid 1px ${Player.config.colors.border};
min-width: 100%;
min-height: 4rem;
box-sizing: border-box;
white-space: pre;
}
.${ns}-settings .${ns}-sub-settings .${ns}-col {
min-height: 1.55rem;
display: flex;
align-items: center;
align-content: center;
white-space: nowrap;
}
.${ns}-settings .${ns}-settings-tabs {
align-items: center;
align-content: center;
justify-content: center;
}
.${ns}-settings .${ns}-settings-tab {
margin: 0.25rem;
text-decoration: underline;
text-align: center;
}
.${ns}-settings .${ns}-settings-tab.active {
font-weight: bold;
}
.${ns}-settings .${ns}-settings-group {
display: none;
}
.${ns}-settings .${ns}-settings-group.active {
display: block;
}
.${ns}-settings .${ns}-host-input {
margin: 0.5rem 0;
border-top: solid 1px ${Player.config.colors.border};
}
.${ns}-settings .${ns}-host-input.invalid {
border: solid 1px red;
}
.${ns}-settings .${ns}-host-input .${ns}-host-controls {
align-items: center;
justify-content: space-between;
margin: 0.125rem 0;
}
.${ns}-settings .${ns}-host-input input[type=text] {
min-width: 100%;
box-sizing: border-box;
}
.${ns}-threads .${ns}-thread-board-list label {
display: inline-block;
width: 4rem;
}
.${ns}-threads .${ns}-thread-list {
margin: 0.5rem -0.25rem 0;
padding: 0.5rem 1rem;
border-top: solid 1px ${Player.config.colors.border};
}
.${ns}-threads .${ns}-thread-list .boardBanner {
margin: 1rem 0;
}
.${ns}-threads table {
border-top: solid 1px ${Player.config.colors.border};
width: 100%;
margin-top: 0.5rem;
border-collapse: collapse;
}
.${ns}-threads table th {
border-bottom: solid 1px ${Player.config.colors.border};
}
.${ns}-threads table th, .${ns}-threads table td {
text-align: left;
padding: 0.25rem;
}
.${ns}-threads table tr {
padding: 0.25rem 0;
}
.${ns}-threads table .${ns}-threads-body tr {
background: ${Player.config.colors.even_row};
}
.${ns}-threads table .${ns}-threads-body tr:nth-child(2n) {
background: ${Player.config.colors.odd_row};
}
.${ns}-create-sound-status {
margin-top: 0.5rem;
border: solid 1px ${Player.config.colors.border};
border-radius: 5px;
padding: 0.25rem;
}
.${ns}-file-input, .${ns}-tools input[type=text] {
width: 100%;
color: black;
}
.${ns}-file-overlay, .${ns}-tools input[type=text] {
box-sizing: border-box;
height: 1.5rem;
border: solid 1px ${Player.config.colors.border};
min-width: 5rem;
display: flex;
align-items: center;
padding: 0 0.25rem;
margin: 0;
}
.${ns}-file-input.placeholder span, .${ns}-create-sound-form input[type=text]::placeholder {
color: #AAA;
opacity: 1;
}
.${ns}-file-input .${ns}-file-overlay {
position: relative;
background: white;
}
.${ns}-file-input .placeholder-text {
display: none;
}
.${ns}-file-input.placeholder .placeholder-text {
display: inherit;
}
.${ns}-file-input span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.${ns}-file-input input[type=file] {
width: 100%;
box-sizing: border-box;
height: 100%;
position: absolute;
left: 0;
opacity: 0;
}
.${ns}-file-input .overfile {
z-index: 9999;
}
.${ns}-file-input .${ns}-file-list {
padding: 0 0.25rem;
}
.${ns}-file-input .${ns}-file-list:empty {
display: none;
}
.${ns}-input-append {
position: absolute;
display: flex;
align-items: center;
background: white;
padding-left: 0.25rem;
right: 0.125rem;
}
.${ns}-threads, .${ns}-settings, .${ns}-tools, .${ns}-player {
display: none;
}
#${ns}-container[data-view-style=settings] .${ns}-settings {
display: block;
}
#${ns}-container[data-view-style=threads] .${ns}-threads {
display: block;
}
#${ns}-container[data-view-style=tools] .${ns}-tools {
display: block;
}
#${ns}-container[data-view-style=image] .${ns}-player,
#${ns}-container[data-view-style=playlist] .${ns}-player,
#${ns}-container[data-view-style=fullscreen] .${ns}-player {
display: block;
}
#${ns}-container[data-view-style=image] .${ns}-list-container, #${ns}-container[data-view-style=image] .${ns}-playlist-search {
display: none;
}
#${ns}-container[data-view-style=image] .${ns}-image-link {
height: auto;
}
#${ns}-container[data-view-style=playlist] .${ns}-image-link {
height: 125px !important;
}
#${ns}-container[data-view-style=fullscreen] .${ns}-image-link {
height: calc(100% - .4rem) !important;
}
#${ns}-container[data-view-style=fullscreen] .${ns}-controls {
position: absolute;
left: 0;
right: 0;
bottom: calc(-2.5rem + .4rem);
opacity: 0.7;
}
#${ns}-container[data-view-style=fullscreen] .${ns}-controls:hover {
bottom: 0;
}`
/***/ }),
/***/ "./src/selectors.js":
/*!**************************!*\
!*** ./src/selectors.js ***!
\**************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = {
'4chan': {
postIdPrefix: 'p',
posts: '.post',
// For 4chan there's native / 4chan X / 4chan X with file info formatting
filename: {
'.fileText .file-info .fnfull': 'textContent',
'.fileText .file-info > a': 'textContent',
'.fileText > a': 'title',
'.fileText': 'textContent'
},
thumb: '.fileThumb',
playLink: {
class: `${ns}-play-link`,
text: 'play',
relative: '.fileText',
prependText: ' '
},
// Deliberately missing dots because this is used to set the class
styleFetcher: 'post reply style-fetcher',
limitWidthOf: '.thread > .postContainer'
},
FoolFuuka: {
postIdPrefix: '',
posts: 'article',
// For the archive the OP and reply selector differs
filename: {
'.thread_image_box .post_file_filename': 'textContent',
'.post_file_filename': 'title'
},
thumb: '.thread_image_link',
playLink: {
class: `${ns}-play-link btnr`,
text: 'Play',
relative: '.post_controls'
},
styleFetcher: 'post_wrapper style-fetcher',
limitWidthOf: '.posts > article.post'
},
Fuuka: {
postIdPrefix: 'p',
posts: '.content > div, td.reply',
filename: {
':scope > span': 'textContent'
},
filenameParser: v => v.split(', ').slice(2).join(', '),
thumb: '.thumb',
playLink: {
class: `${ns}-play-link`,
text: 'play',
relative: 'br:nth-of-type(2)',
before: true,
prependText: ' [',
appendText: ']'
},
styleFetcher: 'reply style-fetcher',
limitWidthOf: '.content > div, .content > table'
}
}[Site];
/***/ }),
/***/ "./src/templates/body.tpl":
/*!********************************!*\
!*** ./src/templates/body.tpl ***!
\********************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = (data = {}) => `<div id="${ns}-container" data-view-style="${Player.config.viewStyle}" style="top: 30px; left: 0px; width: 360px; display: none;">
<div class="${ns}-header ${ns}-row ${ns}-align-center">
${Player.templates.header(data)}
</div>
<div class="${ns}-view-container">
<div class="${ns}-player ${!Player.config.hoverImages ? `${ns}-hide-hover-image` : ''}">
${Player.templates.player(data)}
</div>
<div class="${ns}-settings ${ns}-panel" style="height: 400px">
${Player.templates.settings(data)}
</div>
<div class="${ns}-threads ${ns}-panel" style="height: 400px">
${Player.templates.threads(data)}
</div>
<div class="${ns}-tools ${ns}-panel" style="height: 400px">
${Player.templates.tools(data)}
</div>
</div>
<div class="${ns}-footer">
${Player.templates.footer(data)}
</div>
<input class="${ns}-add-local-file-input" type="file" style="display: none" accept="image/*,.webm" multiple>
</div>`
/***/ }),
/***/ "./src/templates/colorpicker.tpl":
/*!***************************************!*\
!*** ./src/templates/colorpicker.tpl ***!
\***************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(Icons) {module.exports = (data = {}) => `<div class="${ns}-colorpicker dialog" style="top: 0px; left: 0px;">
<div class="${ns}-cp-saturation" style="height: ${data.HEIGHT}px; width: ${data.WIDTH}px;">
<div class="position" style="left: ${data.WIDTH - 3}px; top: -3px;"></div>
</div>
<div class="${ns}-cp-hue" style="height: ${data.HEIGHT}px">
<div class="position"></div>
</div>
<div class="${ns}-output" style="text-align: right;">
<a href="#" class="${ns}-close-colorpicker">${Icons.close}</a>
<div class="output-color" style="background: rgb(${data.rgb[0]}, ${data.rgb[1]}, ${data.rgb[2]});"></div>
<table>
<tr><td>R:</td><td><input type="text" class="${ns}-rgb-input" data-color="0" value="${data.rgb[0]}"></td></tr>
<tr><td>G:</td><td><input type="text" class="${ns}-rgb-input" data-color="1" value="${data.rgb[1]}"></td></tr>
<tr><td>B:</td><td><input type="text" class="${ns}-rgb-input" data-color="2" value="${data.rgb[2]}"></td></tr>
<tr><td>A:</td><td><input type="text" class="${ns}-rgb-input" data-color="3" value="${data.rgb[3]}"></td></tr>
</table>
<button class="${ns}-apply-colorpicker">Apply</button><br>
</div>
</div>`
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/icons */ "./src/icons.js")))
/***/ }),
/***/ "./src/templates/controls.tpl":
/*!************************************!*\
!*** ./src/templates/controls.tpl ***!
\************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(Icons) {module.exports = (data = {}) => `<div class="${ns}-col-auto">
<div class="${ns}-media-control ${ns}-previous-button ${ns}-hover-fill" data-hide-id="previous">
${Icons.skipStart} ${Icons.skipStartFill}
</div>
<div class="${ns}-media-control ${ns}-play-button ${ns}-hover-fill ${!Player.audio || Player.audio.paused ? `${ns}-play` : ''}">
${Icons.play} ${Icons.pause} ${Icons.playFill} ${Icons.pauseFill}
</div>
<div class="${ns}-media-control ${ns}-next-button ${ns}-hover-fill" data-hide-id="next">
${Icons.skipEnd} ${Icons.skipEndFill}
</div>
</div>
<div class="${ns}-col" data-hide-id="seek-bar">
<div class="${ns}-seek-bar ${ns}-progress-bar">
<div class="${ns}-full-bar">
<div class="${ns}-loaded-bar"></div>
<div class="${ns}-current-bar"></div>
</div>
</div>
</div>
<div class="${ns}-col-auto" data-hide-id="time">
<span>
<span class="${ns}-current-time">0:00</span>
<span class="${ns}-text-muted" data-hide-id="duration"> / <span class="${ns}-duration">0:00</span></span>
</span>
</div>
<div class="${ns}-col-auto" data-hide-id="mute">
<div class="${ns}-media-control ${ns}-volume-button ${ns}-hover-fill up" data-hide-id="volume-button">
${Icons.volumeMute} ${Icons.volumeMuteFill}
${Icons.volumeUp} ${Icons.volumeUpFill}
</div>
<div class="${ns}-volume-bar ${ns}-progress-bar" data-hide-id="volume-bar">
<div class="${ns}-full-bar">
<div class="${ns}-current-bar" style="width: ${Player.audio.volume * 100}%"></div>
</div>
</div>
</div>
<div class="${ns}-col-auto" data-hide-id="fullscreen">
<div class="${ns}-media-control ${ns}-fullscreen-button">
${Icons.fullscreen} ${Icons.fullscreenExit}
</div>
</div>`
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/icons */ "./src/icons.js")))
/***/ }),
/***/ "./src/templates/footer.tpl":
/*!**********************************!*\
!*** ./src/templates/footer.tpl ***!
\**********************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = (data = {}) => Player.userTemplate.build({
template: Player.config.footerTemplate,
location: 'footer',
sound: Player.playing
})
+ `<div class="${ns}-expander"></div>`
/***/ }),
/***/ "./src/templates/header.tpl":
/*!**********************************!*\
!*** ./src/templates/header.tpl ***!
\**********************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = (data = {}) => Player.userTemplate.build({
template: Player.config.headerTemplate,
location: 'header',
sound: Player.playing,
defaultName: '4chan Sounds',
outerClass: `${ns}-col-auto`
});
/***/ }),
/***/ "./src/templates/host_input.tpl":
/*!**************************************!*\
!*** ./src/templates/host_input.tpl ***!
\**************************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = (data = {}) => {
// data is the host name
const host = Player.config.uploadHosts[data];
if (!host) {
return '';
}
return `<div class="${ns}-row ${ns}-col ${ns}-host-input ${host.invalid ? 'invalid' : ''}" data-host-name="${data}">
<div class="${ns}-row ${ns}-host-controls">
<div class="${ns}-col-auto">
<label>
<input type="checkbox" data-property="defaultUploadHost" ${Player.config.defaultUploadHost === data ? 'checked': ''}>
Default
</label>
</div>
<div class="${ns}-col-auto"><a href="#" class="${ns}-heading-action ${ns}-remove-host" data-handler="settings.removeHost" data-property="uploadHosts">Remove</a></div>
</div>
<div class="${ns}-row">
<div class="${ns}-col"><input type="text" data-property="uploadHosts" name="name" value="${data || ''}" placeholder="Name"></div>
<div class="${ns}-col"><input type="text" data-property="uploadHosts" name="url" value="${host.url || ''}" placeholder="URL"></div>
<div class="${ns}-col"><input type="text" data-property="uploadHosts" name="responsePath" value="${host.responsePath || ''}" placeholder="Response Path"></div>
<div class="${ns}-col"><input type="text" data-property="uploadHosts" name="responseMatch" value="${host.responseMatch || ''}" placeholder="Response Match"></div>
<div class="${ns}-col"><input type="text" data-property="uploadHosts" name="soundUrl" value="${host.soundUrl || ''}" placeholder="File URL Format"></div>
</div>
<div class="${ns}-row">
<div class="${ns}-col"><textarea data-property="uploadHosts" name="data" placeholder="Data (JSON)">${JSON.stringify(host.data, null, 4)}</textarea></div>
</div>
<div class="${ns}-row">
<div class="${ns}-col"><textarea data-property="uploadHosts" name="headers" placeholder="Headers (JSON)">${host.headers ? JSON.stringify(host.headers, null, 4) : ''}</textarea></div>
</div>
</div>`;
}
/***/ }),
/***/ "./src/templates/item_menu.tpl":
/*!*************************************!*\
!*** ./src/templates/item_menu.tpl ***!
\*************************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = (data = {}) => `<div class="${ns}-menu dialog" id="menu" tabindex="0" data-type="post" style="position: fixed;">
<a class="${ns}-remove-link entry focused" href="javascript:;" data-id="${data.sound.id}">Remove</a>
${data.sound.post ? `<a class="entry" href="#${data.postIdPrefix + data.sound.post}">Show Post</a>` : ''}
<div class="entry has-submenu">
Open
<div class="dialog submenu" style="inset: 0px auto auto 100%;">
<a class="entry" href="${data.sound.image}" target="_blank">Image</a>
<a class="entry" href="${data.sound.src}" target="_blank">Sound</a>
</div>
</div>
<div class="entry has-submenu">
Download
<div class="dialog submenu" style="inset: 0px auto auto 100%;">
<a class="${ns}-download-link entry" href="javascript:;" data-src="${data.sound.image}" data-name="${data.sound.filename}">Image</a>
<a class="${ns}-download-link entry" href="javascript:;" data-src="${data.sound.src}" data-name="${data.sound.name}">Sound</a>
</div>
</div>
<div class="entry has-submenu">
Filter
<div class="dialog submenu" style="inset: 0px auto auto 100%;">
${data.sound.imageMD5 ? `<a class="${ns}-filter-link entry" href="javascript:;" data-filter="${data.sound.imageMD5}">Image</a>` : ''}
<a class="${ns}-filter-link entry" href="javascript:;" data-filter="${data.sound.src.replace(/^(https?\:)?\/\//, '')}">Sound</a>
</div>
</div>
</div>`
/***/ }),
/***/ "./src/templates/list.tpl":
/*!********************************!*\
!*** ./src/templates/list.tpl ***!
\********************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = (data = {}) => (data.sounds || Player.sounds).map(sound =>
`<div class="${ns}-list-item ${ns}-row ${sound.playing ? 'playing' : ''}" data-id="${sound.id}" ${!Player.playlist.matchesSearch(sound) ? 'style="display: none"' : ''} draggable="true">
${Player.userTemplate.build({
template: Player.config.rowTemplate,
location: 'item-' + sound.id,
sound,
outerClass: `${ns}-col-auto`
})}
</div>`
).join('')
/***/ }),
/***/ "./src/templates/player.tpl":
/*!**********************************!*\
!*** ./src/templates/player.tpl ***!
\**********************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(Icons) {module.exports = (data = {}) => `<div class="${ns}-media">
<a class="${ns}-image-link" target="_blank">
<img class="${ns}-image" src="data:image/svg+xml;base64,${btoa(Icons.fcSounds)}"></img>
<video class="${ns}-video"></video>
</a>
<div class="${ns}-controls ${ns}-row">
${Player.templates.controls(data)}
</div>
</div>
<input class="${ns}-playlist-search" type="input" placeholder="Search" style="min-width: 100%; box-sizing: border-box; ${!Player.config.showPlaylistSearch ? 'display: none;' : ''}">
<div class="${ns}-list-container" style="height: 100px">
${Player.templates.list(data)}
</div>
<img class="${ns}-hover-image">`
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/icons */ "./src/icons.js")))
/***/ }),
/***/ "./src/templates/settings.tpl":
/*!************************************!*\
!*** ./src/templates/settings.tpl ***!
\************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {module.exports = (data = {}) => {
const settingsConfig = __webpack_require__(/*! config */ "./src/config/index.js");
const groups = settingsConfig.reduce((groups, setting) => {
if (setting.displayGroup) {
groups[setting.displayGroup] || (groups[setting.displayGroup] = []);
groups[setting.displayGroup].push(setting);
}
return groups;
}, {});
let tpl = `<div class="${ns}-settings-tabs ${ns}-row">
${Object.keys(groups).map(name =>
`<a href="javascript:;" class="${ns}-col-auto ${ns}-settings-tab ${Player.settings.view !== name ? '' : 'active'}" data-group="${name}">${name}</a>`
).join(' | ')}
| <a href="${Player.settings.changelog}" class="${ns}-col-auto ${ns}-settings-tab" target="_blank">v${"3.2.1"}</a>
</div>`;
Object.keys(groups).forEach(name => {
tpl += `<div class="${ns}-settings-group ${Player.settings.view !== name ? '' : 'active'}" data-group="${name}">`;
groups[name].forEach(function addSetting(setting) {
// Filter settings with a null display method;
if (setting.displayMethod === null) {
return;
}
const desc = setting.description;
tpl += `
<div class="${ns}-row ${ns}-align-${setting.isSubSetting ? 'start' : 'center'} ${setting.isSubSetting ? `${ns}-sub-settings` : ''}">
<div class="${ns}-col ${!setting.isSubSetting ? `${ns}-heading` : `${ns}-space-between`} ${desc ? `${ns}-has-description` : ''}" ${desc ? `title="${desc.replace(/"/g, '"')}"` : ''}>
${setting.title}
${!setting.actions || !setting.actions.length ? '' : `<div style="display: inline-block; margin-right: .25rem">
${(setting.actions || []).map(action => `<a href="#" class="${ns}-heading-action" data-handler="${action.handler}" data-property="${setting.property}">${action.title}</a>`).join(' ')}
</div>`}
</div>`;
if (setting.text) {
tpl += setting.dismissTextId
? `<div class="${ns}-col" style="min-width: 100%">`
+ Player.display.ifNotDismissed(
setting.dismissTextId,
setting.dismissRestoreText,
`<div data-dismiss-id="${setting.dismissTextId}">`
+ setting.text
+ `<a href="javascript:;" class="${ns}-dismiss-link" data-dismiss="${setting.dismissTextId}" style="display:block; margin-top:.25rem">Dismiss</a>`
+ `</div>`
)
+ `</div>`
: setting.text;
};
if (setting.settings) {
setting.settings.forEach(subSetting => addSetting({
...setting,
actions: null,
settings: null,
description: null,
...subSetting,
isSubSetting: true
}));
} else {
let value = _.get(Player.config, setting.property, setting.default),
attrs = (setting.attrs || '') + (setting.class ? ` class="${setting.class}"` : '') + ` data-property="${setting.property}"`,
displayMethod = setting.displayMethod,
displayMethodFunction = typeof displayMethod === 'function' ? displayMethod : _.get(Player, displayMethod);
if (setting.format) {
value = Player.getHandler(setting.format)(value);
}
let type = typeof value;
if (setting.split) {
value = value.join(setting.split);
} else if (type === 'object') {
value = JSON.stringify(value, null, 4);
}
tpl += typeof displayMethodFunction === 'function'
? displayMethodFunction(value, attrs)
: type === 'boolean'
? `<div class="${ns}-col"><input type="checkbox" ${attrs} ${value ? 'checked' : ''}></div>`
: displayMethod === 'textarea' || type === 'object'
? `<div class="${ns}-row ${ns}-col"><textarea ${attrs}>${value}</textarea></div>`
: setting.options
? `<div class="${ns}-col">
<select ${attrs}>
${Object.keys(setting.options).map(k => `<option value="${k}" ${value === k ? 'selected' : ''}>
${setting.options[k]}
</option>`).join('')}
</select>
</div>`
: `<div class="${ns}-col"><input type="text" ${attrs} value="${value}"></div>`;
}
tpl += '</div>';
});
tpl += '</div>';
});
return tpl;
}
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./src/templates/thread_boards.tpl":
/*!*****************************************!*\
!*** ./src/templates/thread_boards.tpl ***!
\*****************************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = (data = {}) => (Player.threads.boardList || []).map(board => {
let checked = Player.threads.selectedBoards.includes(board.board);
return !checked && !Player.threads.showAllBoards
? ''
: `<label>
<input type="checkbox" value="${board.board}" ${checked ? 'checked' : ''}>
/${board.board}/
</label>`
}).join('')
/***/ }),
/***/ "./src/templates/thread_list.tpl":
/*!***************************************!*\
!*** ./src/templates/thread_list.tpl ***!
\***************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(_) {module.exports = (data = {}) => Object.keys(Player.threads.displayThreads).reduce((rows, board) => {
return rows.concat(Player.threads.displayThreads[board].map(thread => `
<tr>
<td>
<a class="quotelink" href="//boards.${thread.ws_board ? '4channel' : '4chan'}.org/${thread.board}/thread/${thread.no}#p${thread.no}" target="_blank">
>>>/${thread.board}/${thread.no}
</a>
</td>
<td>${thread.sub || ''}</td>
<td>${thread.replies} / ${thread.images}</td>
<td>${_.timeAgo(thread.time)}</td>
<td>${_.timeAgo(thread.last_modified)}</td>
</tr>
`))
}, []).join('')
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/_ */ "./src/_.js")))
/***/ }),
/***/ "./src/templates/threads.tpl":
/*!***********************************!*\
!*** ./src/templates/threads.tpl ***!
\***********************************/
/*! no static exports found */
/***/ (function(module, exports) {
module.exports = (data = {}) => `<div class="${ns}-heading ${ns}-has-description" title="Search for threads with a sound OP">
Active Threads
${!Player.threads.loading ? `- <a class="${ns}-fetch-threads-link ${ns}-heading-action" href="#">Update</a>` : ''}
</div>
<div style="display: ${Player.threads.loading ? 'block' : 'none'}">Loading</div>
<div style="display: ${Player.threads.loading ? 'none' : 'block'}">
<div class="${ns}-heading ${ns}-has-description" title="Only includes threads containing the search.">
Filter
</div>
<input type="text" class="${ns}-threads-filter" value="${Player.threads.filterValue || ''}"></input>
<div class="${ns}-heading">
Boards - <a class="${ns}-all-boards-link ${ns}-heading-action" href="#">${Player.threads.showAllBoards ? 'Selected Only' : 'Show All'}</a>
</div>
<div class="${ns}-thread-board-list">
${Player.templates.threadBoards(data)}
</div>
${!Player.threads.hasParser
? ''
: `<div class="${ns}-heading" style="text-align: center">
${Player.config.threadsViewStyle !== 'table'
? `<a class="${ns}-threads-view-style ${ns}-heading-action" style="margin: 0" data-style="table" href="#">Table</a>`
: `<span>Table</span>`}
|
${Player.config.threadsViewStyle !== 'board'
? `<a class="${ns}-threads-view-style ${ns}-heading-action" style="margin: 0" data-style="board" href="#">Board</a>`
: `<span>Board</span>`}
</div>`
}
${
!Player.threads.hasParser || Player.config.threadsViewStyle === 'table'
? `<table>
<tr>
<th>Thread</th>
<th>Subject</th>
<th>Replies/Images</th>
<th>Started</th>
<th>Updated</th>
<tr>
<tbody class="${ns}-threads-body"></tbody>
</table>`
: `<div class="${ns}-thread-list"></div>`
}
</div>`
/***/ }),
/***/ "./src/templates/tools.tpl":
/*!*********************************!*\
!*** ./src/templates/tools.tpl ***!
\*********************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(Icons) {module.exports = (data = {}) => `<div class="${ns}-heading">Encode / Decode URL</div>
<div class="${ns}-row">
<div class="${ns}-col"><input type="text" class="${ns}-decoded-input" placeholder="https://"></div>
<div class="${ns}-col"><input type="text" class="${ns}-encoded-input" placeholder="https%3A%2F%2F"></div>
</div>
<div class="${ns}-heading">
Create Sound Image
</div>
<div class="${ns}-create-sound-form">
<div class="${ns}-row" style="margin-bottom: .5rem">
${Player.display.ifNotDismissed('createSoundDetails', 'Show Help',
`<div class="${ns}-col" data-dismiss-id="createSoundDetails">
Select an image and sound to combine as a sound image.
The sound will be uploaded to the selected file host and the url will be added to the image filename.
If you have an account for a host that you would like to use then make the required changes in the <a class="${ns}-host-setting-link" href="#">host config</a>.
That typically means providing a user token in the data or headers.<br>
${Player.tools.hasFFmpeg
? 'Selecting a webm with audio as the image will split it into a video only webm to be posted and ogg audio file to be uploaded.'
: 'For a webm with audio first split the webm into a separate video and audio file and select them both.'
}
<br>
Multiple sound files, or a comma-separated list of sound URLs, can be given for a single image.
If you do have multiple sounds the name will also be a considered comma-separated list.<br>
<a href="javascript:;" class="${ns}-dismiss-link" data-dismiss="createSoundDetails">Dismiss</a>
</div>`)}
</div>
<div class="${ns}-row">
Host
</div>
<div class="${ns}-row">
<div class="${ns}-col">
<select class="${ns}-create-sound-host">
${Object.keys(Player.config.uploadHosts).map((hostId, i) =>
Player.config.uploadHosts[hostId] && !Player.config.uploadHosts[hostId].invalid
? `<option value="${hostId}" ${Player.config.defaultUploadHost === hostId ? 'selected' : ''}>${hostId}</option>`
: ''
).join('')}
</select>
</div>
</div>
<div class="${ns}-row" style="margin-top: .25rem">
Data
</div>
<div class="${ns}-row">
<div class="${ns}-col">
<input type="text" class="${ns}-create-sound-name" placeholder="Name/s">
</div>
</div>
<div class="${ns}-row">
<div class="${ns}-col">
<div class="${ns}-file-input placeholder">
<div class="${ns}-file-overlay">
<span class="placeholder-text">Select/Drop Image</span>
<span class="text"></span>
<input class="${ns}-create-sound-img" type="file" accept="image/*,.webm">
</div>
</div>
</div>
<div class="${ns}-col">
<div class="${ns}-file-input placeholder" ${Player.tools.useSoundURL ? 'display: none;' : ''}>
<div class="${ns}-file-overlay">
<span class="placeholder-text">Select/Drop Sound/s</span>
<span class="text"></span>
<div class="overfile ${ns}-input-append">
${Player.tools.hasFFmpeg && `<label class="${ns}-use-video-label" style="display: none;">Use video<input type="checkbox" class="${ns}-use-video"></label>` || ''}
<a href="#" class="${ns}-toggle-sound-input" data-type="url" title="Enter a URL of a previously uploaded file.">${Icons.link}</a>
</div>
<input class="${ns}-create-sound-snd" type="file" accept="audio/*,video/*" multiple>
</div>
<div class="${ns}-file-list"></div>
</div>
<div class="${ns}-row ${ns}-align-center" style="position: relative; ${Player.tools.useSoundURL ? '' : 'display: none;'}">
<a href="#" class="${ns}-toggle-sound-input ${ns}-input-append" data-type="file" title="Select a file to upload.">
${Icons.fileEarmarkMusic}
</a>
<input type="text" class="${ns}-create-sound-snd-url" placeholder="Sound URL/s" style="min-width: 100%;">
</div>
</div>
</div>
<div class="${ns}-row" style="margin-top: .5rem">
<div class="${ns}-col-auto">
<button class="${ns}-create-button">Create</button>
</div>
</div>
</div>
<div class="${ns}-create-sound-status" ${Player.tools.createStatusText ? '' : 'style="display: none"'}>
${Player.tools.createStatusText}
</div>`
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/icons */ "./src/icons.js")))
/***/ }),
/***/ "./src/templates/views_menu.tpl":
/*!**************************************!*\
!*** ./src/templates/views_menu.tpl ***!
\**************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(Icons) {module.exports = (data = {}) => `<div class="${ns}-menu dialog" id="menu" tabindex="0" data-type="post" style="position: fixed;">
${[ 'playlist', 'image' ].includes(Player.config.viewStyle) ? ''
: `<a class="${ns}-row nowrap ${ns}-align-center ${ns}-player-button entry" href="javascript:;"><div class="${ns}-col">Player</div><div class="${ns}-col-auto">${Icons.musicNoteList}</div></a>`}
${Player.config.viewStyle === 'settings' ? ''
: `<a class="${ns}-row nowrap ${ns}-align-center ${ns}-config-button entry" href="javascript:;"><div class="${ns}-col">Settings</div><div class="${ns}-col-auto">${Icons.gear}</div></span></a>`}
${Player.config.viewStyle === 'threads' ? ''
: `<a class="${ns}-row nowrap ${ns}-align-center ${ns}-threads-button entry" href="javascript:;"><div class="${ns}-col">Threads</div><div class="${ns}-col-auto">${Icons.search}</div></span></a>`}
${Player.config.viewStyle === 'tools' ? ''
: `<a class="${ns}-row nowrap ${ns}-align-center ${ns}-tools-button entry" href="javascript:;"><div class="${ns}-col">Tools</div><div class="${ns}-col-auto">${Icons.tools}</div></span></a>`}
</div>`
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./src/icons */ "./src/icons.js")))
/***/ })
/******/ });