// ==UserScript== // @name 4chan sounds player // @version 3.5.0 // @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/* // @match *://archive.nyafuu.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 archive-media-0.nyafuu.org // @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 z.zz.fo // @connect zz.ht // @connect too.lewd.se // @connect lewd.se // @connect * // @run-at document-start // @require https://gitcdn.link/repo/richtr/NoSleep.js/07fcee254724ab1b79076fbc22f3dd447649a2eb/dist/NoSleep.min.js // @require https://gitcdn.link/repo/Stuk/jszip/7bbcb3873db23f6d27550cdbb6c4cc2bdeb32194/dist/jszip.js // @require https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js // @downloadURL https://update.greasyfork.icu/scripts/402682/4chan%20sounds%20player.user.js // @updateURL https://update.greasyfork.icu/scripts/402682/4chan%20sounds%20player.meta.js // ==/UserScript== /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ /***/ "./node_modules/bootstrap-icons/icons/arrow-clockwise.svg": /*!****************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/arrow-clockwise.svg ***! \****************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/arrow-down.svg": /*!***********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/arrow-down.svg ***! \***********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/arrow-repeat.svg": /*!*************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/arrow-repeat.svg ***! \*************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/arrow-up.svg": /*!*********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/arrow-up.svg ***! \*********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/arrows-collapse.svg": /*!****************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/arrows-collapse.svg ***! \****************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/arrows-expand.svg": /*!**************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/arrows-expand.svg ***! \**************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/bootstrap-reboot.svg": /*!*****************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/bootstrap-reboot.svg ***! \*****************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/box-arrow-in-left.svg": /*!******************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/box-arrow-in-left.svg ***! \******************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/box-arrow-right.svg": /*!****************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/box-arrow-right.svg ***! \****************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/chat-right-quote.svg": /*!*****************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/chat-right-quote.svg ***! \*****************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/check-square.svg": /*!*************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/check-square.svg ***! \*************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/chevron-down.svg": /*!*************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/chevron-down.svg ***! \*************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/chevron-up.svg": /*!***********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/chevron-up.svg ***! \***********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/file-earmark-image.svg": /*!*******************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/file-earmark-image.svg ***! \*******************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/file-earmark-music.svg": /*!*******************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/file-earmark-music.svg ***! \*******************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/filter.svg": /*!*******************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/filter.svg ***! \*******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/fullscreen-exit.svg": /*!****************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/fullscreen-exit.svg ***! \****************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/fullscreen.svg": /*!***********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/fullscreen.svg ***! \***********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/gear.svg": /*!*****************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/gear.svg ***! \*****************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/github.svg": /*!*******************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/github.svg ***! \*******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/image.svg": /*!******************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/image.svg ***! \******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/info-circle.svg": /*!************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/info-circle.svg ***! \************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/layout-text-window.svg": /*!*******************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/layout-text-window.svg ***! \*******************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/link-45deg.svg": /*!***********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/link-45deg.svg ***! \***********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/music-note-list.svg": /*!****************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/music-note-list.svg ***! \****************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/pause-fill.svg": /*!***********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/pause-fill.svg ***! \***********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/pause.svg": /*!******************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/pause.svg ***! \******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/play-fill.svg": /*!**********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/play-fill.svg ***! \**********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/play.svg": /*!*****************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/play.svg ***! \*****************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/plus-circle.svg": /*!************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/plus-circle.svg ***! \************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/search.svg": /*!*******************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/search.svg ***! \*******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/shuffle.svg": /*!********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/shuffle.svg ***! \********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/skip-end-fill.svg": /*!**************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/skip-end-fill.svg ***! \**************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/skip-end.svg": /*!*********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/skip-end.svg ***! \*********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/skip-start-fill.svg": /*!****************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/skip-start-fill.svg ***! \****************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/skip-start.svg": /*!***********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/skip-start.svg ***! \***********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/soundwave.svg": /*!**********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/soundwave.svg ***! \**********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/speaker.svg": /*!********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/speaker.svg ***! \********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/square.svg": /*!*******************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/square.svg ***! \*******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/tools.svg": /*!******************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/tools.svg ***! \******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/trash.svg": /*!******************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/trash.svg ***! \******************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/volume-mute-fill.svg": /*!*****************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/volume-mute-fill.svg ***! \*****************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/volume-mute.svg": /*!************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/volume-mute.svg ***! \************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/volume-up-fill.svg": /*!***************************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/volume-up-fill.svg ***! \***************************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/volume-up.svg": /*!**********************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/volume-up.svg ***! \**********************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n \n \n"); /***/ }), /***/ "./node_modules/bootstrap-icons/icons/x.svg": /*!**************************************************!*\ !*** ./node_modules/bootstrap-icons/icons/x.svg ***! \**************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("\n \n"); /***/ }), /***/ "./src/_.js": /*!******************!*\ !*** ./src/_.js ***! \******************/ /***/ ((module) => { 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; } if (path === '') { return object; } const props = path.split('.'); const lastProp = props.pop(); const parent = props.reduce((obj, k) => obj && obj[k], object); return parent && lastProp in parent ? 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); hours && minutes < 10 && (minutes = '0' + minutes); 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, position = 'beforeend') { let el; if (html instanceof Node) { el = html; } else { const container = document.createElement('div'); container.innerHTML = html; el = container.children[0]; } parent && parent.insertAdjacentElement(position, el); el instanceof Element && _.elementHandler(el); return el; }; module.exports.elementHTML = function elementHTML(el, content) { el.innerHTML = content; _.elementHandler(el); }; module.exports.elementHandler = function elementHandler(el) { // Wire up resize elements. el.querySelectorAll(`.${ns}-expander`).forEach(el => { el.classList.add('no-touch-action'); Player.events.set(el, 'pointdragstart', 'position.initResize'); Player.events.set(el, 'pointdrag.unbound', 'position.doResize'); Player.events.set(el, 'pointdragend', 'position.stopResize'); }); // Wire up popovers. const popovers = Array.from(el.querySelectorAll(`.${ns}-popover`)); el.classList.contains(`${ns}-popover`) && (popovers.push(el)); popovers.forEach(popover => { popover.addEventListener('mouseenter', Player.display._popoverMouseEnter); popover.addEventListener('mouseleave', Player.display._popoverMouseLeave); popover.nodeName !== 'A' && popover.addEventListener('click', Player.display._popoverClick); }); // Wire up events from attributes. Player.events.apply(el); }; module.exports.escAttr = function (str, escapeDoubleQuote) { return str .replace(';', ';') .replace(/'/g, ''') .replace(/"/g, escapeDoubleQuote ? '\\"' : '"') .replace(/\n/g, '\\n'); }; /***/ }), /***/ "./src/api.js": /*!********************!*\ !*** ./src/api.js ***! \********************/ /***/ ((module) => { 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/index.js": /*!*****************************************!*\ !*** ./src/components/actions/index.js ***! \*****************************************/ /***/ ((module) => { module.exports = { atRoot: [ 'togglePlay', 'play', 'pause', 'next', 'previous', 'stop', 'toggleMute', 'volumeUp', 'volumeDown' ], public: [ 'togglePlay', 'play', 'pause', 'next', 'previous', 'stop', 'toggleMute', 'volumeUp', 'volumeDown' ], initialize() { // Keep this reference to switch Player.audio to standalone videos and back. Player.controls._audio = Player.audio; }, /** * Switching being playing and paused. */ togglePlay() { if (Player.audio.paused) { Player.play(); } else { Player.pause(); } }, /** * Start playback. */ async play(sound, { paused } = {}) { try { // Handle id instead of sound object. if (typeof sound === 'string') { sound = Player.sounds.find(s => s.id === sound); } // If nothing is currently selected to play start playing the first sound. if (!sound && !Player.playing && Player.sounds.length) { sound = Player.sounds[0]; } // If a new sound is being played update the display. if (sound && sound !== Player.playing) { if (Player.playing) { Player.playing.playing = false; } // Remove play on load listeners for the previous sound. Player.video.removeEventListener('canplaythrough', Player.actions.playOnceLoaded); Player.audio.removeEventListener('canplaythrough', Player.actions.playOnceLoaded); // 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); Player.video.removeEventListener(evt, handlerFunction); sound.standaloneVideo && Player.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.video.loop = !Player.isStandalone; Player.audio = sound.standaloneVideo ? Player.video : Player.controls._audio; Player.audio._linked = Player.isVideo && !Player.isStandalone && Player.video; Player.video._linked = Player.isVideo && !Player.isStandalone && Player.audio; Player.container.classList[Player.isVideo ? 'add' : 'remove']('playing-video'); Player.container.classList[Player.isVideo || sound.image.endsWith('gif') ? 'add' : 'remove']('playing-animated'); 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 && (Player.video.readyState < 3 || Player.audio.readyState < 3)) { Player.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 only start playback once the video and audio are both loaded. */ playOnceLoaded(e) { if (e.currentTarget.readyState > 3 && e.currentTarget._linked.readyState > 3) { e.currentTarget.removeEventListener('canplaythrough', Player.actions.playOnceLoaded); e.currentTarget._linked.removeEventListener('canplaythrough', Player.actions.playOnceLoaded); e.currentTarget._inlinePlayer && e.currentTarget._inlinePlayer.pendingControls && e.currentTarget._inlinePlayer.pendingControls(); e.currentTarget._linked.play(); e.currentTarget.play(); } else { !e.currentTarget.paused && e.currentTarget.pause(); !e.currentTarget._linked.paused && e.currentTarget._linked.pause(); e.currentTarget.currentTime !== 0 && (e.currentTarget.currentTime = 0); e.currentTarget._linked.currentTime !== 0 && (e.currentTarget._linked.currentTime = 0); } }, /** * Pause playback. */ pause() { Player.audio && Player.audio.pause(); }, /** * Stop playback. */ stop() { Player.audio.src = null; Player.playing = null; Player.isVideo = false; Player.isStandalone = false; Player.trigger('stop'); }, /** * Play the next sound. */ next(opts) { Player.actions._movePlaying(1, opts); }, /** * Play the previous sound. */ previous(opts) { // Over three seconds into a sound restarts it instead. const restartSeconds = typeof Player.config.restartSeconds == 'number' && Player.config.restartSeconds; if (restartSeconds && Player.audio.currentTime > restartSeconds) { Player.audio.currentTime = 0; } else { Player.actions._movePlaying(-1, opts); } }, _movePlaying(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() { Player.audio.volume = Math.min(Player.audio.volume + 0.05, 1); }, /** * Lower the volume by 5%. */ volumeDown() { 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() { Player.audio.volume = (Player._lastVolume || 0.5) * !Player.audio.volume; } }; /***/ }), /***/ "./src/components/controls/index.js": /*!******************************************!*\ !*** ./src/components/controls/index.js ***! \******************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); module.exports = { template: __webpack_require__(/*! ./templates/controls.tpl */ "./src/components/controls/templates/controls.tpl"), audioEvents: { ended: () => Player.config.autoplayNext && Player.next(), pause: 'controls.handleMediaEvent', play: 'controls.handleMediaEvent', seeked: 'controls.handleMediaEvent', waiting: 'controls.handleMediaEvent', ratechange: 'controls.handleMediaEvent', timeupdate: 'controls.updateDuration', loadedmetadata: [ 'controls.updateDuration', 'controls' ], durationchange: 'controls.updateDuration', volumechange: 'controls.updateVolume', loadstart: 'controls.pollForLoading', error: 'controls.handleAudioError' }, actions: { previous: 'previous({ force: true })', playPause: 'togglePlay', next: 'next({ force: true })', seek: 'controls.handleSeek($event, "main")', mute: 'toggleMute', volume: 'controls.handleVolume($event, "main")', fullscreen: 'display.toggleFullScreen' }, async initialize() { // 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', () => { Player.video = Player.$(`.${ns}-video`); Player.video.dataset.id = 'main'; // Keep track of heavily updated elements. Player.audio.volumeBar = Player.video.volumeBar = Player.$(`.${ns}-volume-bar .${ns}-current-bar`); Player.audio.currentTimeBar = Player.video.currentTimeBar = Player.$(`.${ns}-seek-bar .${ns}-current-bar`); Player.audio.loadedBar = Player.video.loadedBar = Player.$(`.${ns}-seek-bar .${ns}-loaded-bar`); // Set the initial volume/seek bar positions and hidden controls. Player.controls.updateDuration({ currentTarget: Player.audio }); Player.controls.updateVolume({ currentTarget: Player.audio }); 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(); }); // Sync audio/video when the tab is focused. document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { Player.controls.sync(Player.audio); } }); }, /** * Handle audio errors */ handleAudioError(err) { if (Player.playing) { Player.logError(`Failed to play ${Player.playing.title}. Please check the console for details.`, err, 'warning'); Player.playing.error = err; setTimeout(() => Player.next({ paused: true }), 3000); } }, /** * Handle audio events. Sync the video up, and update the controls. */ handleMediaEvent(e) { const audio = e.currentTarget._inlineAudio || e.currentTarget; Player.controls.sync(e.currentTarget); Player.controls.updateDuration(e); document.querySelectorAll(`.${ns}-play-button[data-audio="${audio.dataset.id}"]`).forEach(el => { el.classList[audio.paused ? 'add' : 'remove'](`${ns}-play`); }); }, /** * Sync the webm to the audio. Matches the videos time and play state to the audios. */ sync(from) { const to = from._linked; if (from && from.readyState > 3 && to && to.readyState > 3) { to.currentTime = from.currentTime % to.duration; to[from.paused ? 'pause' : 'play'](); to.playbackRate = from.playbackRate; to.defaultPlaybackRate = from.defaultPlaybackRate; } }, /** * Poll for how much has loaded. I know there's the progress event but it unreliable. */ pollForLoading() { Player._loadingPoll = Player._loadingPoll || setInterval(Player.controls.updateLoaded, 1000); }, /** * Stop polling for how much has loaded. */ stopPollingForLoading() { Player._loadingPoll && clearInterval(Player._loadingPoll); Player._loadingPoll = null; }, /** * Update the loading bar. */ updateLoaded() { 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.audio.loadedBar.style.width = size + '%'; }, /** * Update the seek bar and the duration labels. */ updateDuration(e) { const media = e.currentTarget; const audio = media._inlineAudio || media; const controls = media._inlinePlayer ? media._inlinePlayer.controls : document; const currentTime = _.toDuration(media.currentTime); const duration = _.toDuration(media.duration); const audioId = audio.dataset.id; // Gross use of childNodes to avoid textContent triggering mutation observers of other scripts. controls && controls.querySelectorAll(`.${ns}-current-time[data-audio="${audioId}"]`).forEach(el => el.childNodes[0].textContent = currentTime); controls && controls.querySelectorAll(`.${ns}-duration[data-audio="${audioId}"]`).forEach(el => el.childNodes[0].textContent = duration); Player.controls.updateProgressBarPosition(audio.currentTimeBar, media.currentTime, media.duration); }, /** * Update the volume bar. */ updateVolume(e) { const audio = e.currentTarget._inlineAudio || e.currentTarget; const controls = audio._inlinePlayer ? audio._inlinePlayer.controls : Player.container; const vol = audio.volume; // Store volume of the main player. if (audio === Player.audio) { vol > 0 && (Player._lastVolume = vol); GM.setValue('volume', vol); } controls && controls.querySelectorAll(`.${ns}-volume-button[data-audio="${audio.dataset.id}"]`).forEach(el => { el.classList[vol === 0 ? 'add' : 'remove']('mute'); el.classList[vol > 0 ? 'add' : 'remove']('up'); }); Player.controls.updateProgressBarPosition(audio.volumeBar, 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(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(e, audioId) { const media = audioId === 'main' ? Player.audio : Player.inline.audio[audioId]._inlinePlayer.master; if (media && media.duration && media.duration !== Infinity) { media.currentTime = media.duration * Player.controls._getBarXRatio(e); } }, /** * Handle the user interacting with the volume bar. */ handleVolume(e, audioId) { const audio = audioId === 'main' ? Player.audio : Player.inline.audio[audioId]; audio.volume = Player.controls._getBarXRatio(e); }, _getBarXRatio(e) { const offset = 0.4 * Player.remSize; const offsetX = e.offsetX || (e.targetTouches[0].pageX - e.currentTarget.getBoundingClientRect().left); return Math.max(0, Math.min(1, (offsetX - offset) / (parseInt(getComputedStyle(e.currentTarget).width, 10) - (2 * offset)))); }, /** * Set all controls visible. */ showAllControls() { Player.$all(`.${ns}-controls [data-hide-id]`).forEach(el => el.style.display = null); }, /** * Hide elements in the controls instead of wrapping */ preventWrapping() { 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() { 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); } }; /***/ }), /***/ "./src/components/display/index.js": /*!*****************************************!*\ !*** ./src/components/display/index.js ***! \*****************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const cssTemplate = __webpack_require__(/*! ../../scss/style.scss */ "./src/scss/style.scss"); const cssVarsTemplate = __webpack_require__(/*! ../../scss/root.scss */ "./src/scss/root.scss"); const css4chanXPolyfillTemplate = __webpack_require__(/*! ../../scss/4chan-x-polyfill.scss */ "./src/scss/4chan-x-polyfill.scss"); const menus = { themes: __webpack_require__(/*! ./templates/themes_menu.tpl */ "./src/components/display/templates/themes_menu.tpl"), views: __webpack_require__(/*! ./templates/views_menu.tpl */ "./src/components/display/templates/views_menu.tpl") }; const dismissedContentCache = {}; const dismissedRestoreCache = {}; const noSleep = typeof NoSleep === 'function' && new NoSleep(); const enableNoSleep = () => noSleep.enable(); const disableNoSleep = () => noSleep.disable(); module.exports = { atRoot: [ 'show', 'hide' ], public: [ 'show', 'hide' ], template: __webpack_require__(/*! ./templates/body.tpl */ "./src/components/display/templates/body.tpl"), _noSleepEnabled: false, async initialize() { try { Player.display.dismissed = (await GM.getValue('dismissed')).split(','); } catch (err) { Player.display.dismissed = []; } // Reset marquees when a new sound is played. Player.on('playsound', () => { Player.display._marquees = {}; !Player.display._marqueeTO && Player.display.runTitleMarquee(); }); // Store the rem size Player.remSize = parseFloat(getComputedStyle(document.documentElement).fontSize); // Set up no sleep Player.on('config:preventSleep', Player.display._initNoSleep); Player.display._initNoSleep(Player.config.preventSleep); // Close dialogs when the user clicks anywhere or presses escape. document.body.addEventListener('click', Player.display.closeDialogs); document.body.addEventListener('keydown', e => e.key === 'Escape' && Player.display.closeDialogs(e)); // Update fullscreen details when details are changed/loaded Player.on('playsound', Player.display.updateFullScreenDetails); Player.on('tags-loaded', sound => sound === Player.playing && Player.display.updateFullScreenDetails()); }, /** * Create the player show/hide button in to the 4chan X header. */ createPlayerButton() { 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('
  • Sounds
  • ', 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); _.element('Sounds', br, 'beforebegin'); br.parentNode.insertBefore(document.createTextNode(']'), br); } else if (isChanX) { // Add a button in the header for 4chan X. _.element(` Sounds `, document.getElementById('shortcut-settings'), 'beforebegin'); } else { // Add a [Sounds] link in the top and bottom nav for native 4chan. document.querySelectorAll('#settingsWindowLink, #settingsWindowLinkBot').forEach(function (link) { _.element('Sounds', link, 'beforebegin'); link.parentNode.insertBefore(document.createTextNode('] ['), link); }); } }, /** * Render the player. */ async render() { try { if (Player.container) { document.body.removeChild(Player.container); document.head.removeChild(Player.stylesheet); } // Create the main stylesheet. Player.stylesheet = Player.stylesheet || _.element('', document.head); Player.stylesheet.innerHTML = (!isChanX ? '/* 4chanX Polyfill */\n\n' + css4chanXPolyfillTemplate() : '') + '\n\n/* Sounds Player CSS */\n\n' + cssTemplate(); Player.display.updateCSSVars(); // Create the main player. For native threads put it in the threads to get free quote previews. const parent = Thread && !isChanX && document.body.querySelector('.board') || document.body; Player.container = _.element(Player.display.template(), parent); await Player.trigger('rendered'); } catch (err) { Player.logError('There was an error rendering the sound player.', err); // Can't recover, throw. throw err; } }, updateCSSVars() { // Insert the stylesheet if it doesn't exist. 4chan X polyfill, sound player styling, and user styling. Player.varsCSS = Player.varsCSS || _.element('', document.head); Player.varsCSS.innerHTML = '\n\n/* Sounds Player CSS variables */\n\n' + cssVarsTemplate(); }, /** * Change what view is being shown */ async setViewStyle(style) { // Get the size and style prior to switching. const previousStyle = Player.config.viewStyle; // 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); if (style === 'playlist' || style === 'image') { Player.controls.preventWrapping(); } // Try to reapply the pre change sizing unless it was fullscreen. if (previousStyle !== 'fullscreen' || style === 'fullscreen') { const [ width, height ] = (await GM.getValue('size') || '').split(':'); width && height && Player.position.resize(parseInt(width, 10), parseInt(height, 10)); Player.position.setPostWidths(); } Player.trigger('view', style, previousStyle); }, /** * Togle the display status of the player. */ toggle() { 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() { 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. */ async show() { if (!Player.container.style.display) { return; } Player.container.style.display = null; Player.isHidden = false; await Player.trigger('show'); }, /** * Stop playback and close the player. */ async close() { Player.stop(); Player.hide(); }, /** * Toggle the video/image and controls fullscreen state */ async toggleFullScreen() { if (!document.fullscreenElement) { // Make sure the player (and fullscreen contents) are visible first. if (Player.isHidden) { Player.show(); } Player.$(`.${ns}-player`).requestFullscreen(); document.body.addEventListener('pointermove', Player.display._fullscreenMouseMove); Player.display._fullscreenMouseMove(); } else if (document.exitFullscreen) { document.exitFullscreen(); document.body.removeEventListener('pointermove', Player.display._fullscreenMouseMove); } }, _fullscreenMouseMove() { Player.container.classList.remove('cursor-inactive'); Player.display.fullscreenCursorTO && clearTimeout(Player.display.fullscreenCursorTO); Player.display.fullscreenCursorTO = setTimeout(function () { Player.container.classList.add('cursor-inactive'); }, 2000); }, updateFullScreenDetails() { const tags = Player.playing.tags || {}; document.querySelector('.fullscreen-details').innerHTML = [ Player.playing.name, [ tags.title, tags.artist ].filter(Boolean).join(' - ') ].filter(Boolean).join(' • ') || Player.playing.title; }, /** * Handle the fullscreen state being changed */ _handleFullScreenChange() { 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(); }, async restore(restore) { const restoreIndex = Player.display.dismissed.indexOf(restore); if (restore && restoreIndex > -1) { Player.display.dismissed.splice(restoreIndex, 1); Player.$all(`[\\@click^='display.restore("${restore}")']`).forEach(el => { _.element(dismissedContentCache[restore], el, 'beforebegin'); el.parentNode.removeChild(el); }); await GM.setValue('dismissed', Player.display.dismissed.join(',')); } }, async dismiss(dismiss) { if (dismiss && !Player.display.dismissed.includes(dismiss)) { Player.display.dismissed.push(dismiss); Player.$all(`[data-dismiss-id="${dismiss}"]`).forEach(el => { _.element(`${dismissedRestoreCache[dismiss]}`, el, 'beforebegin'); el.parentNode.removeChild(el); }); await GM.setValue('dismissed', Player.display.dismissed.join(',')); } }, ifNotDismissed(name, restore, text) { dismissedContentCache[name] = text; dismissedRestoreCache[name] = restore; return Player.display.dismissed.includes(name) ? `${restore}` : text; }, /** * Display a menu */ showMenu(relative, menu, parent) { const dialog = typeof menu === 'string' ? _.element(menus[menu]()) : menu; 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', e => { Player.display._setFocusedMenuItem(e); el.dispatchEvent(new CustomEvent('entry-focus')); }); }); // Allow clicks of sub menus dialog._keepOpenFor = Array.from(dialog.querySelectorAll('.entry.has-submenu')); dialog._closeFor = Array.from(dialog.querySelectorAll('.submenu')); Player.trigger('menu-open', dialog); }, _setFocusedMenuItem(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'); el.dispatchEvent(new CustomEvent('entry-blur')); }); 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'; } }, /** * Close any open menus. */ closeDialogs(e) { document.querySelectorAll(`.${ns}-dialog`).forEach(dialog => { const clickableElements = (dialog._keepOpenFor || []).concat(dialog.dataset.allowClick ? dialog : []); // Close the dialog if there's no event... const closeDialog = !e // ...the event was not for an element that allows the dialog to stay open || !clickableElements.find(el => el === e.target || el.contains(e.target)) // ...or the event was for an element explicitly set to close the dialog. || (dialog._closeFor || []).find(el => el === e.target || el.contains(e.target)); if (closeDialog) { dialog.parentNode.removeChild(dialog); Player.trigger('menu-close', dialog); } }); }, async runTitleMarquee() { 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'; }); }, _popoverMouseEnter: e => { const icon = e.currentTarget; const wide = icon.classList.contains('wide'); if (!icon.infoEl || !Player.container.contains(icon.infoEl)) { icon.infoEl = _.element(`
    ${icon.dataset.content}
    `, Player.container); icon.infoEl._keepOpenFor = [ icon ]; Player.position.showRelativeTo(icon.infoEl, icon); } }, _popoverMouseLeave: e => { const icon = e.currentTarget; if (icon.infoEl && !icon.infoEl._clicked) { icon.infoEl.parentNode.removeChild(icon.infoEl); delete icon.infoEl; } }, _popoverClick: e => { const icon = e.currentTarget; const openPopover = icon.infoEl && Player.container.contains(icon.infoEl); if (!openPopover) { Player.display._popoverMouseEnter(e); } else if (!(icon.infoEl._clicked = !icon.infoEl._clicked)) { Player.display._popoverMouseLeave(e); } }, _initNoSleep: newValue => { const action = newValue ? 'addEventListener' : 'removeEventListener'; if (!noSleep || !!newValue === Player.display._noSleepEnabled) { return; } Player.audio[action]('play', enableNoSleep); Player.audio[action]('pause', disableNoSleep); Player.audio[action]('ended', disableNoSleep); Player.display._noSleepEnabled = !!newValue; if (!Player.audio.paused) { noSleep[newValue ? 'enable' : 'disable'](); } }, untz() { const container = Player.$(`.${ns}-image-link`); Player.untzing = !Player.untzing; Player.audio.playbackRate = Player.audio.defaultPlaybackRate = Player.untzing ? 1.3 : 1; Player.container.classList[Player.untzing ? 'add' : 'remove']('untz'); if (Player.untzing) { const overlay = Player.$('.image-color-overlay'); let rotate = 0; overlay.style.filter = `brightness(1.5); hue-rotate(${rotate}deg)`; (function color() { overlay.style.filter = `hue-rotate(${rotate = 360 - rotate}deg)`; Player.untzColorTO = setTimeout(color, 500); }()); (function bounce() { if (Player.untzing) { container.style.transform = `scale(${1 + Math.random() * 0.05})`; container.style.filter = `brightness(${1 + Math.random() * 0.5}) blur(${Math.random() * 3}px)`; Player.untzBounceTO = setTimeout(bounce, 200); } }()); } else { container.style.transform = null; container.style.filter = null; clearTimeout(Player.untzBounceTO); clearTimeout(Player.untzColorTO); } } }; /***/ }), /***/ "./src/components/events/index.js": /*!****************************************!*\ !*** ./src/components/events/index.js ***! \****************************************/ /***/ ((module) => { module.exports = { atRoot: [ 'on', 'off', 'trigger' ], // Holder of event handlers. _events: { }, initialize() { const eventLocations = { Player, ...Player.components }; const audio = []; for (let comp of Object.values(eventLocations)) { comp.audioEvents && audio.push(comp.audioEvents); } // Clear mousedown listeners when the mouse/touch is released. document.body.addEventListener('pointerup', Player.events.clearMousedown); document.body.addEventListener('pointercancel', Player.events.clearMousedown); Player.on('rendered', function () { // 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))); } } }); }, /** * Add event listeners from event attributes on an elements and all it's decendents. * * @param {Element} element The element to set event listeners for. */ apply(element) { // Find all elements with event attributes, including the given element. const eventAttrs = []; const elAttrs = document.evaluate('.//attribute::*[starts-with(name(), "@")]', element, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < elAttrs.snapshotLength; i++) { eventAttrs.push(elAttrs.snapshotItem(i)); } const childAttrs = document.evaluate('.//*/attribute::*[starts-with(name(), "@")]', element, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < childAttrs.snapshotLength; i++) { eventAttrs.push(childAttrs.snapshotItem(i)); } for (let attr of eventAttrs) { Player.events.set(attr.ownerElement, attr.name.slice(1), attr.value); } }, set(el, attr, action) { action = action.trim(); const [ evt, ...modsArr ] = attr.split('.'); const mods = modsArr.reduce((m, n) => { m[n] = true; return m; }, {}); // Remove listener already set. const listeners = el._eventListeners || (el._eventListeners = {}); listeners[evt] && el.removeEventListener(evt, listeners[evt]); // If the action is JS lazily create a script element to get the handler. Avoids CSP blocking new Function. let handler = action && (Player.getHandler(action) || function ($event) { const script = document.createElement('script'); script.innerText = `window.${ns}Handler = function(Player, $event) { with (Player) { ${action} } };`; document.head.appendChild(script); handler = unsafeWindow[`${ns}Handler`].bind(null, Player); delete unsafeWindow[`${ns}Handler`]; handler($event); }); const listener = function (evt) { if (mods.prevent) { evt.preventDefault(); } if (mods.stop) { evt.stopPropagation(); } if (mods.disabled && evt.currentTarget.classList.contains('disabled')) { evt.currentTarget.classList.contains('disabled'); } return handler && handler.call(this, evt, Player); }; // Point drag is a special case to handle pointer dragging. if (evt === 'pointdrag') { const downListener = e => { // No idea why but this seems to fire twice. So avoid that. if (!e._dragInit) { e._dragInit = true; listeners.pointdragstart && listeners.pointdragstart(e); if (!e.preventDrag) { el.setPointerCapture(e.pointerId); Player._mousedown = el; Player._mousedownListener = listener; Player._mousedownMoveEl = mods.unbound ? document.documentElement : el; Player._mousedownMoveEl.addEventListener('pointermove', listener, mods); el.addEventListener('pointerleave', listener, mods); mods.boxed && el.addEventListener('pointerleave', Player.events.clearMousedown); !mods.move && listener(e); } } }; el.addEventListener('pointerdown', downListener); listeners.pointerdown = downListener; } else if (evt === 'pointdragstart' || evt === 'pointdragend') { listeners[evt] = listener; } else { el.addEventListener(evt, listener, mods); listeners[evt] = listener; } }, /** * Create an event listener on the player. * * @param {String} evt The name of the events. * @param {function} handler The handler function. */ on(evt, handler) { const evts = Array.isArray(evt) ? evt : [ evt ]; evts.forEach(evt => { 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(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. */ async trigger(evt, ...data) { const events = Player.events._events[evt] || []; for (let handler of events) { await handler(...data); } }, clearMousedown(e) { if (Player._mousedown) { Player._mousedown.releasePointerCapture(e.pointerId); Player._mousedownMoveEl.removeEventListener('pointermove', Player._mousedownListener); Player._mousedown.removeEventListener('pointerleave', Player._mousedownListener); Player._mousedown._eventListeners.pointdragend && Player._mousedown._eventListeners.pointdragend(e); Player._mousedown = Player._mousedownListener = null; } } }; /***/ }), /***/ "./src/components/footer/index.js": /*!****************************************!*\ !*** ./src/components/footer/index.js ***! \****************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); module.exports = { template: () => Player.userTemplate.build({ template: Player.config.footerTemplate + `
    `, location: 'footer', sound: Player.playing, defaultName: '4chan Sounds', outerClass: `${ns}-col-auto` }), initialize() { Player.userTemplate.maintain(Player.footer, 'footerTemplate'); }, render() { _.elementHTML(Player.$(`.${ns}-footer`), Player.footer.template()); } }; /***/ }), /***/ "./src/components/header/index.js": /*!****************************************!*\ !*** ./src/components/header/index.js ***! \****************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); module.exports = { template: () => Player.userTemplate.build({ template: Player.config.headerTemplate + `
    `, location: 'header', sound: Player.playing, defaultName: '4chan Sounds', outerClass: `${ns}-col-auto` }), initialize() { Player.userTemplate.maintain(Player.header, 'headerTemplate'); }, render() { _.elementHTML(Player.$(`.${ns}-header`), Player.header.template()); } }; /***/ }), /***/ "./src/components/hotkeys/index.js": /*!*****************************************!*\ !*** ./src/components/hotkeys/index.js ***! \*****************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { const settingsConfig = __webpack_require__(/*! config */ "./src/config/index.js"); let keyConfigs; module.exports = { _keyMap: { ' ': 'space', arrowleft: 'left', arrowright: 'right', arrowup: 'up', arrowdown: 'down' }, initialize() { Player.on('rendered', Player.hotkeys.apply); Player.on('config:hotkeys', Player.hotkeys.apply); keyConfigs = settingsConfig.reduce((c, s) => { s.property === 'hotkey_bindings' && s.settings.forEach(s => c[s.property.slice(16)] = s); return c; }, {}); // 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', Player.hotkeys.setMediaMetadata); Player.audio.addEventListener('ratechange', Player.hotkeys.setMediaPosition); Player.audio.addEventListener('seeked', Player.hotkeys.setMediaPosition); Player.on('tags-loaded', sound => sound === Player.playing && Player.hotkeys.setMediaMetadata()); } }, async setMediaMetadata() { const sound = Player.playing; const tags = sound.tags || {}; navigator.mediaSession.playbackState = 'playing'; const metadata = { title: tags.title || sound.name || sound.title, artist: tags.artist || `/${Board}/ - ${Thread || '4chan Sounds Player'}`, album: tags.album || document.title, artwork: [ { src: Player.playing.thumb, sizes: '125x125' } ] }; // If it's not a video add the full image to artwork. (TODO: grab the first frame for videos) // If we have the dimensions already add the artwork, otherwise load them then reset the metadata. if (!Player.isVideo) { if (sound._fullDimension) { metadata.artwork.push({ src: Player.playing.image, sizes: sound._fullDimension }); } else { const img = new Image(); img.onload = function () { sound._fullDimension = img.width + 'x' + img.height; Player.hotkeys.setMediaMetadata(); }; img.src = Player.playing.image; } } navigator.mediaSession.metadata = new MediaMetadata(metadata); Player.hotkeys.setMediaPosition(); }, setMediaPosition() { navigator.mediaSession.setPositionState({ duration: Player.audio.duration || 0, playbackRate: Player.audio.playbackRate, position: Player.audio.currentTime }); }, 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() { 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 open 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(e) { // Ignore events on inputs so you can still type. if (Player.isHidden && (Player.config.hotkeys !== 'always' || !Player.sounds.length)) { return; } const inputFocused = [ 'INPUT', 'SELECT', 'TEXTAREA', 'INPUT' ].includes(e.target.nodeName) const k = e.key.toLowerCase(); const bindings = Player.config.hotkey_bindings || {}; // Look for a matching hotkey binding Object.entries(bindings).forEach(function checkBinding([ name, keyDef ]) { if (Array.isArray(keyDef)) { return keyDef.find(_def => checkBinding([ name, _def ])); } const bindingConfig = k === keyDef.key && (!!keyDef.shiftKey === !!e.shiftKey) && (!!keyDef.ctrlKey === !!e.ctrlKey) && (!!keyDef.metaKey === !!e.metaKey) && (!keyDef.ignoreRepeat || !e.repeat) && keyConfigs[name]; if (bindingConfig && (!inputFocused || bindingConfig.allowFocusedInput)) { e.preventDefault(); e._binding = keyDef; Player.getHandler(bindingConfig.keyHandler)(e); } }); }, /** * Turn a hotkey definition or key event into an input string. */ stringifyKey(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(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/inline/index.js": /*!****************************************!*\ !*** ./src/components/inline/index.js ***! \****************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const selectors = __webpack_require__(/*! ../../selectors */ "./src/selectors.js"); const controlsTemplate = __webpack_require__(/*! ../controls/templates/controls.tpl */ "./src/components/controls/templates/controls.tpl"); module.exports = { idx: 0, audio: { }, expandedNodes: [ ], // Similar but not exactly the audio events in the controls component. mediaEvents: { ended: evt => Player.inline._movePlaying(evt.currentTarget.dataset.id, +(Player.config.expandedRepeat !== 'one')), pause: 'controls.handleMediaEvent', play: 'controls.handleMediaEvent', seeked: 'controls.handleMediaEvent', waiting: 'controls.handleMediaEvent', timeupdate: 'controls.updateDuration', loadedmetadata: 'controls.updateDuration', durationchange: 'controls.updateDuration', volumechange: 'controls.updateVolume' }, initialize() { if (!is4chan) { return; } Player.inline.observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { mutation.addedNodes.forEach(Player.inline.handleAddedNode); mutation.removedNodes.forEach(Player.inline.handleRemovedNode); }); }); Player.on('config:playExpandedImages', Player.inline._handleConfChange); Player.on('config:playHoveredImages', Player.inline._handleConfChange); Player.inline._handleConfChange(); }, /** * Start/stop observing for hover images when a dependent conf is changed. */ _handleConfChange() { if (Player.config.playExpandedImages || Player.config.playHoveredImages) { Player.inline.start(); } else { Player.inline.stop(); } }, /** * Check if an added node is an expanded/hover sound image and play the audio. * * @param {Element} node Added node. */ handleAddedNode(node) { try { if (node.nodeName !== 'IMG' && node.nodeName !== 'VIDEO') { return; } const isExpandedImage = Player.config.playExpandedImages && node.matches(selectors.expandedImage); const isHoverImage = Player.config.playHoveredImages && node.matches(selectors.hoverImage); if (isExpandedImage || isHoverImage) { const isVideo = node.nodeName === 'VIDEO'; let id; try { // 4chan X images have the id set. Handy. // Otherwise get the parent post, looking up the image link for native hover images, and the id from it. id = isChanX ? node.dataset.fileID.split('.')[1] : (isExpandedImage ? node : document.querySelector(`a[href$="${node.src.replace(/^https?:/, '')}"]`)) .closest(selectors.posts).id.slice(selectors.postIdPrefix.length); } catch (err) { return; } // Check for sounds added to the player. let sounds = id && Player.sounds.filter(s => s.post === id && !s.standaloneVideo) || []; if (Player.config.expandedAllowFiltered) { sounds = sounds.concat(Player.filteredSounds.filter(s => s.post === id && !s.disallow.host)); } if (!sounds.length) { return; } // Create a new audio element. const audio = new Audio(sounds[0].src); const aId = audio.dataset.id = Player.inline.idx++; const master = isVideo && Player.config.expandedLoopMaster === 'video' ? node : audio; Player.inline.audio[aId] = audio; // Remember this node is playing audio. Player.inline.expandedNodes.push(node); // Add some data and cross link the nodes. node.classList.add(`${ns}-has-inline-audio`); node._inlineAudio = audio; audio._inlinePlayer = node._inlinePlayer = { master, video: node, isVideo, audio, sounds, index: 0 }; // Link video & audio so they sync. if (isVideo) { node._linked = audio; audio._linked = node; } // Start from the beginning taking the volume from the main player. audio.src = sounds[0].src; audio.volume = Player.audio.volume; audio.currentTime = 0; // Add the sync handlers to which source is master. Player.inline.updateSyncListeners(master, 'add'); // Show the player controls for expanded images/videos. const showPlayerControls = isExpandedImage && Player.config.expandedControls; if (isVideo && showPlayerControls) { // Remove the default controls, and remove them again when 4chan X tries to add them. node.controls = false; node.controlsObserver = new MutationObserver(() => node.controls = false); node.controlsObserver.observe(node, { attributes: true }); // Play/pause the audio instead when the video is clicked. node.addEventListener('click', () => Player.inline.playPause(aId)); } // For videos wait for both to load before playing. if (isVideo && (node.readyState < 3 || audio.readyState < 3)) { audio.pause(); node.pause(); // Set the add controls function so playOnceLoaded can run it when it's ready. node._inlinePlayer.pendingControls = showPlayerControls && addControls; node.addEventListener('canplaythrough', Player.actions.playOnceLoaded); audio.addEventListener('canplaythrough', Player.actions.playOnceLoaded); } else { showPlayerControls && addControls(); audio.play(); } function addControls() { delete node._inlinePlayer.pendingControls; node.parentNode.classList.add(`${ns}-has-controls`); // Create the controls and store the bars on the audio node for reference. Avoid checking the DOM. const controls = audio._inlinePlayer.controls = _.element(controlsTemplate({ audio, multiple: sounds.length > 1, audioId: aId, inline: true, actions: { previous: `inline.previous("${aId}")`, playPause: `inline.playPause("${aId}")`, next: `inline.next("${aId}")`, seek: `controls.handleSeek($event, "${aId}")`, mute: `inline.mute("${aId}")`, volume: `controls.handleVolume($event, "${aId}")` } }), node.parentNode); // Don't want to close the expanded image or open the image when the controls are clicked. controls.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); }); audio.volumeBar = controls.querySelector(`.${ns}-volume-bar .${ns}-current-bar`); audio.currentTimeBar = controls.querySelector(`.${ns}-seek-bar .${ns}-current-bar`); Player.controls.updateProgressBarPosition(audio.volumeBar, audio.volume, 1); } } } catch (err) { Player.logError('Failed to play sound.', err); } }, /** * Check if a removed node is an expanded/hover sound image and stop the audio. * * @param {Element} node Added node. */ handleRemovedNode(node) { const nodes = [ node ]; node.querySelectorAll && nodes.push(...node.querySelectorAll(`.${ns}-has-inline-audio`)); nodes.forEach(node => { if (node._inlineAudio) { Player.inline._removeForNode(node); } }); }, _removeForNode(node) { // Stop removing controls. node.controlsObserver && node.controlsObserver.disconnect(); // Stop listening for media events. Player.inline.updateSyncListeners(node._inlinePlayer.master, 'remove'); // Remove controls. const controls = node._inlineAudio._inlinePlayer.controls; if (controls) { controls.parentNode.classList.remove(`${ns}-has-controls`); controls.parentNode.removeChild(controls); } // Stop the audio and cleanup the data. node._inlineAudio.pause(); delete Player.inline.audio[node._inlineAudio.dataset.id]; delete node._inlineAudio; Player.inline.expandedNodes = Player.inline.expandedNodes.filter(n => n !== node); }, /** * Set audio/video sync listeners on a video for an inline sound webm. * * @param {Element} video Video node. * @param {String} action add or remove. */ updateSyncListeners(node, action) { if (node.nodeName === 'VIDEO' || node.nodeName === 'AUDIO') { const audio = node._inlineAudio || node; if (action === 'remove') { const video = audio._inlinePlayer.video; video.removeEventListener('canplaythrough', Player.actions.playOnceLoaded); audio.removeEventListener('canplaythrough', Player.actions.playOnceLoaded); } Object.entries(Player.inline.mediaEvents).forEach(([ event, handler ]) => { node[`${action}EventListener`](event, Player.getHandler(handler)); }); } }, /** * Start observing for expanded/hover images. */ start() { Player.inline.observer.observe(document.body, { childList: true, subtree: true }); }, /** * Stop observing for expanded/hover images. */ stop() { Player.inline.observer.disconnect(); Player.inline.expandedNodes.forEach(Player.inline._removeForNode); Player.inline.expandedNodes = []; }, /** * Handle previous click for inline controls. * * @param {String} audioId Identifier of the inline audio. */ previous(audioId) { const audio = Player.inline.audio[audioId]; if (audio.currentTime > 3) { audio.currentTime = 0; } else { Player.inline._movePlaying(audioId, -1); } }, /** * Handle next click for inline controls. * * @param {String} audioId Identifier of the inline audio. */ next(audioId) { Player.inline._movePlaying(audioId, 1); }, _movePlaying(audioId, dir) { const audio = Player.inline.audio[audioId]; const data = audio && audio._inlinePlayer; const count = data.sounds.length; const repeat = Player.config.expandedRepeat; if (data && (repeat !== 'none' || data.index + dir >= 0 && data.index + dir < count)) { data.index = (data.index + dir + count) % count; audio.src = data.sounds[data.index].src; if (data.controls) { const prev = data.controls.querySelector(`.${ns}-previous-button`); const next = data.controls.querySelector(`.${ns}-next-button`); prev && prev.classList[repeat !== 'all' && data.index === 0 ? 'add' : 'remove']('disabled'); next && next.classList[repeat !== 'all' && data.index === count - 1 ? 'add' : 'remove']('disabled'); } // For videos wait for both to load before playing. if (data.isVideo && (data.video.readyState < 3 || audio.readyState < 3)) { data.master.currentTime = 0; data.master.pause(); data.video.pause(); data.video.addEventListener('canplaythrough', Player.actions.playOnceLoaded); audio.addEventListener('canplaythrough', Player.actions.playOnceLoaded); } else { data.master.currentTime = 0; data.master.play(); } } }, /** * Handle play/pause click for inline controls. * * @param {String} audioId Identifier of the inline audio. */ playPause(audioId) { const audio = Player.inline.audio[audioId]; audio && audio[audio.paused ? 'play' : 'pause'](); }, /** * Handle mute click for inline controls. * * @param {String} audioId Identifier of the inline audio. */ mute(audioId) { const audio = Player.inline.audio[audioId]; audio && (audio.volume = (Player._lastVolume || 0.5) * !audio.volume); } }; /***/ }), /***/ "./src/components/minimised/index.js": /*!*******************************************!*\ !*** ./src/components/minimised/index.js ***! \*******************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = { _showingPIP: false, initialize() { 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() { 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 = _.element(``, document.querySelector('#shortcuts'), 'afterbegin'); } if (Player.config.chanXControls === 'never' || Player.config.chanXControls === 'closed' && !Player.isHidden) { return container.innerHTML = ''; } const audioId = Player.audio.dataset.id; // Render the contents. _.elementHTML(container, Player.userTemplate.build({ template: Player.config.chanXTemplate, location: '4chan-X-controls', sound: Player.playing, replacements: { 'prev-button': `${Icons.skipStart} ${Icons.skipStartFill}`, 'play-button': `${Icons.play} ${Icons.pause} ${Icons.playFill} ${Icons.pauseFill}`, 'next-button': `${Icons.skipEnd} ${Icons.skipEndFill} `, 'sound-current-time': `0:00`, 'sound-duration': `0:00` } })); } }, /** * Move the image to a picture in picture like thumnail. */ showPIP() { 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'; image.style.height = null; // Show the player again when the image is clicked. image.addEventListener('click', Player.minimised._handleImageClick); }, /** * Move the image back to the player. */ hidePIP() { Player.minimised._showingPIP = false; const image = document.querySelector(`.${ns}-image-link`); const controls = Player.$(`.${ns}-controls`); controls.parentNode.insertBefore(document.querySelector(`.${ns}-image-link`), controls); image.classList.remove(`${ns}-pip`); image.style.bottom = null; image.style.height = Player.config.imageHeight + 'px'; image.removeEventListener('click', Player.minimised._handleImageClick); }, _handleImageClick: e => { e.preventDefault(); Player.show(); } }; /***/ }), /***/ "./src/components/playlist/index.js": /*!******************************************!*\ !*** ./src/components/playlist/index.js ***! \******************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); const { postIdPrefix } = __webpack_require__(/*! ../../selectors */ "./src/selectors.js"); const xhrReplacer = __webpack_require__(/*! ../../xhr-replace */ "./src/xhr-replace.js"); const itemMenuTemplate = __webpack_require__(/*! ./templates/item_menu.tpl */ "./src/components/playlist/templates/item_menu.tpl"); module.exports = { atRoot: [ 'add', 'remove' ], public: [ 'search' ], tagLoadTO: {}, template: __webpack_require__(/*! ./templates/player.tpl */ "./src/components/playlist/templates/player.tpl"), listTemplate: __webpack_require__(/*! ./templates/list.tpl */ "./src/components/playlist/templates/list.tpl"), tagsDialogTemplate: __webpack_require__(/*! ./templates/tags_dialog.tpl */ "./src/components/playlist/templates/tags_dialog.tpl"), initialize() { // 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 the hover image element. Player.on('rendered', Player.playlist.afterRender); // Various things to do when a new sound plays. Player.on('playsound', sound => { // Update the image/video. Player.playlist.showImage(sound); // Update the previously and the new playing rows. Player.$all(`.${ns}-list-item.playing, .${ns}-list-item[data-id="${Player.playing.id}"]`).forEach(el => { const newItem = Player.playlist.listTemplate({ sounds: [ Player.sounds.find(s => s.id === el.dataset.id) ] }); _.element(newItem, el, 'beforebegin'); el.parentNode.removeChild(el); }); // If the player isn't fullscreen scroll to the playing item. Player.config.viewStyle !== 'fullscreen' && Player.playlist.scrollToPlaying('nearest'); // Scroll the thread to the playing post. Player.config.autoScrollThread && sound.post && (location.href = location.href.split('#')[0] + '#' + postIdPrefix + sound.post); // Load tags from the audio file. Player.playlist.loadTags(Player.playing.id); }); // 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')); const container = Player.$(`.${ns}-image-link`); container.href = '#'; Player.$(`.${ns}-background-image`).src = Player.video.src = ''; Player.$(`.${ns}-image`).src = `data:image/svg+xml;base64,${btoa(Icons.fcSounds)}`; container.classList.remove(`${ns}-show-video`); }); // Reapply filters when they change Player.on('config:filters', Player.playlist.applyFilters); Player.on('config:allow', 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); // Listen for the playlist being shuffled/ordered. Player.on('config:shuffle', Player.playlist._handleShuffle); // Update an open tags info dialog when tags are loaded for a sound. Player.on('tags-loaded', sound => { const dialog = Player.$(`.tags-dialog[data-sound-id="${sound.id}"]`); dialog && _.elementHTML(dialog, Player.playlist.tagsDialogTemplate(sound)); }); // Resize the image when the config is changed (from other tabs) Player.on('config:imageHeight', height => Player.$(`.${ns}-image-link`).style.height = height + 'px'); // Preload the next audio. Player.on([ 'playsound', 'order' ], () => { const next = Player.sounds[(Player.sounds.indexOf(Player.playing) + 1) % Player.sounds.length]; next && Player.playlist.preload(next); }); // Maintain changes to the user templates it's dependent values Player.userTemplate.maintain(Player.playlist, 'rowTemplate', [ 'shuffle' ]); // Resize observer to handle transparent images Player.playlist.imageResizeObserver = new ResizeObserver(Player.playlist.resizeTransBG); }, /** * Render the playlist. */ render() { _.elementHTML(Player.$(`.${ns}-list-container`), Player.playlist.listTemplate()); Player.playlist.afterRender(); }, afterRender() { Player.playlist.image = Player.$(`.${ns}-image`); Player.playlist.transparentImageBG = Player.$(`.${ns}-image-transparent-bg`); Player.playlist.hoverImage = Player.$(`.${ns}-hover-image`); Player.playlist.imageResizeObserver.disconnect(); Player.playlist.imageResizeObserver.observe(Player.playlist.image); Player.playlist.image.onload = Player.playlist.resizeTransBG; }, /** * Restore the last playlist or image view. */ restore() { Player.display.setViewStyle(Player.playlist._lastView || 'playlist'); }, /** * Update the image displayed in the player. */ showImage(sound) { const container = document.querySelector(`.${ns}-image-link`); const img = container.querySelector(`.${ns}-image`); const background = container.querySelector(`.${ns}-background-image`); img.src = background.src = ''; img.src = background.src = sound.imageOrThumb; Player.isVideo && (Player.video.src = sound.image); if (Player.config.viewStyle !== 'fullscreen') { container.href = sound.image; } container.classList[Player.isVideo ? 'add' : 'remove'](ns + '-show-video'); }, /** * Resize the background element that prevents transparent images display over themself. */ resizeTransBG() { const contentBoxRatio = Player.playlist.image.width / Player.playlist.image.height; const imageSizeRatio = Player.playlist.image.naturalWidth / Player.playlist.image.naturalHeight; const bgEl = Player.playlist.transparentImageBG; bgEl.style.width = Math.min(imageSizeRatio / contentBoxRatio * 100, 100) + '%'; bgEl.style.height = Math.min(contentBoxRatio / imageSizeRatio * 100, 100) + '%'; }, /** * Switch between playlist and image view. */ toggleView(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(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(`
    ${Player.playlist.listTemplate({ sounds: [ sound ] })}
    `); 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); } }, addFromDrop(e) { for (let item of e.dataTransfer.items) { const entry = item.getAsEntry ? item.getAsEntry() : item.webkitGetAsEntry(); entry && Player.playlist._scanEntry(entry); } }, _scanEntry(entry) { if (entry.isDirectory) { return Player.playlist._readEntries(entry.createReader()); } return entry.file(file => Player.playlist.addFromFiles([ file ])); }, _readEntries(reader) { reader.readEntries(entries => { if (entries.length) { entries.forEach(Player.playlist._scanEntry); Player.playlist._readEntries(reader); } }); }, addFromFiles(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('seeked', function () { canvas.width = video.videoWidth; canvas.height = video.videoHeight; context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight); thumbSrc = canvas.toDataURL(); _continue(); }); video.src = imageSrc; video.currentTime = 0.001; function _continue() { const { sounds } = Player.posts.getSounds(file.name, imageSrc, null, thumbSrc, null, true); sounds.forEach(sound => Player.add({ ...sound, local: true, type })); } }); }, selectLocalFiles() { Player.$(`.${ns}-add-local-file-input`).click(); }, /** * Remove a sound */ remove(sound) { // Accept the sound object or id if (typeof sound !== 'object') { sound = Player.sounds.find(s => s.id === '' + 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. const item = sound && Player.$(`.${ns}-list-item[data-id="${sound.id}"]`); item && Player.$(`.${ns}-list-container`).removeChild(item); sound && Player.trigger('remove', sound); }, toggleRepeat() { const values = [ 'all', 'one', 'none' ]; const current = values.indexOf(Player.config.repeat); Player.set('repeat', values[(current + 4) % 3]); }, toggleShuffle() { Player.set('shuffle', !Player.config.shuffle); }, _handleShuffle() { // 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'); }, /** * Handle an playlist item being clicked. Either open/close the menu or play the sound. */ handleSelect(e) { // Ignore if a link was clicked. if (e.target.nodeName === 'A' || e.target.closest('a')) { return; } const id = e.currentTarget.getAttribute('data-id'); const sound = id && Player.sounds.find(sound => sound.id === id); sound && Player.play(sound); }, /** * Display an item menu. */ handleItemMenu(e, 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.currentTarget.closest(`.${ns}-list-container`); const parent = listContainer || Player.container; // Create the menu. const html = itemMenuTemplate({ sound, postIdPrefix }); const dialog = _.element(html, parent); const relative = e.currentTarget.classList.contains(`${ns}-item-menu-button`) ? e.currentTarget : e; Player.display.showMenu(relative, dialog, parent); }, /** * Toggle the hoverImages setting */ toggleHoverImages(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() { 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(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(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() { Player.playlist.hoverImage.style.display = 'none'; }, /** * Start dragging a playlist item. */ handleDragStart(e) { Player.playlist._dragging = e.currentTarget; Player.playlist.setHoverImageVisibility(); e.currentTarget.classList.add(`${ns}-dragging`); const img = document.createElement('img'); img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; img.opacity = 0; e.dataTransfer.setDragImage(img, 0, 0); e.dataTransfer.dropEffect = 'move'; e.dataTransfer.setData('text/plain', e.currentTarget.getAttribute('data-id')); }, /** * Swap a playlist item when it's dragged over another item. */ handleDragEnter(e) { if (!Player.playlist._dragging) { return; } 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 needs inserting before the node after the one it's dropped on. const position = moving.compareDocumentPosition(before); if (position & 0x04) { before = before.nextElementSibling; } // 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(e) { if (!Player.playlist._dragging) { return; } delete Player.playlist._dragging; e.currentTarget.classList.remove(`${ns}-dragging`); Player.playlist.setHoverImageVisibility(); }, /** * Scroll to the playing item, unless there is an open menu in the playlist. */ scrollToPlaying(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() { // Check for added sounds that are now filtered. Player.sounds.forEach(sound => { sound.disallow = Player.disallowedSound(sound); if (sound.disallow) { Player.playlist.remove(sound); Player.filteredSounds.push(sound); Player.posts.updateButtons(sound.post); } }); // Check for filtered sounds that are now accepted. Player.filteredSounds.forEach((sound, idx) => { sound.disallow = Player.disallowedSound(sound); if (!sound.disallow) { Player.filteredSounds.splice(idx, 1); Player.playlist.add(sound); Player.posts.updateButtons(sound.post); } }); Player.trigger('filters-applied'); }, // Add a filter. addFilter(filter) { filter && Player.set('filters', Player.config.filters.concat(filter)); }, /** * Search the playlist */ _handleSearch(e) { Player.playlist.search(e.currentTarget.value.toLowerCase()); }, search(v) { const lastSearch = Player.playlist._lastSearch; Player.playlist._lastSearch = v; if (v === lastSearch) { return; } Player.playlist.render(); }, matchesSearch(sound) { const v = Player.playlist._lastSearch; return !v || sound.title.toLowerCase().includes(v) || sound.post && String(sound.post.toLowerCase()).includes(v) || String(sound.src.toLowerCase()).includes(v); }, toggleSearch(show) { const input = Player.$(`.${ns}-playlist-search`); !show && Player.playlist._lastSearch && Player.playlist.search(); input.style.display = show ? null : 'none'; show && input.focus(); }, /** * Attempt to load info tags from a sound source. * @param {String} id The sound id */ loadTags(id) { const sound = Player.sounds.find(s => s.id === id); // Fall out if they've already been loaded. if (sound.tags) { return; } // Wait a bit before fetching to ignore the mouse going across. Player.playlist.tagLoadTO[id] = setTimeout(() => { const reader = new jsmediatags.Reader(sound.src); // Replace XMLHttpRequest to avoid cors. reader._findFileReader().prototype._createXHRObject = () => new xhrReplacer.GM(); // Load and read the tags. reader.read({ onSuccess: handleTags, onError: handleTags }); }, 150); function handleTags(data) { // Store all the string tags that jsmediatags has set. sound.tags = data && Object.entries(data.tags || {}).reduce((tags, [ name, value ]) => { typeof value === 'string' && (tags[name] = value); return tags; }, {}); Player.trigger('tags-loaded', sound); } }, /** * Cancel a pending of tags for a sond. * @param {String} id The sound id */ abortTags(id) { clearTimeout(Player.playlist.tagLoadTO[id]); delete Player.playlist.tagLoadTO[id]; }, /** * Set a few initial values to being resizing the playlist image. */ expandImageStart(e) { if (e.button === 0 && !Player.isHidden && Player.config.viewStyle === 'playlist') { Player.$(`.${ns}-image-link`).style.cursor = 'ns-resize'; Player._imageResizeStartY = (e.touches && e.touches[0] || e).clientY; Player._imageResizeStartHeight = Player.config.imageHeight; Player._imageResized = false; Player._imageReizeMaxHeight = Player.$(`.${ns}-player`).getBoundingClientRect().height - Player.$(`.${ns}-controls`).getBoundingClientRect().height; } }, /** * Resize the playlist image. */ expandImage(e) { if (!Player.isHidden && Player.config.viewStyle === 'playlist') { Player._imageResized = true; const clientY = (e.touches && e.touches[0] || e).clientY; const height = (Player._imageResizeStartHeight + clientY - Player._imageResizeStartY); Player.$(`.${ns}-image-link`).style.height = Math.min(Math.max(125, height), Player._imageReizeMaxHeight) + 'px'; } }, /** * Keep the image within the player. */ setImageHeight() { if (!Player.isHidden && Player.config.viewStyle === 'playlist') { Player.$(`.${ns}-image-link`).style.cursor = null; const imageLink = Player.$(`.${ns}-image-link`); const height = parseInt(imageLink.style.height); const maxHeight = Player.$(`.${ns}-player`).getBoundingClientRect().height - Player.$(`.${ns}-controls`).getBoundingClientRect().height; const finalHeight = Math.max(125, Math.min(height, maxHeight)); imageLink.style.height = finalHeight + 'px'; Player.set('imageHeight', finalHeight); } }, /** * If a click on the image link was after resizing then don't open the image. */ expandImageClick(e) { !Player.isHidden && Player.config.viewStyle === 'playlist' && Player._imageResized && e.preventDefault(); }, /** * Preload a sound. */ async preload(sound) { if (sound.preloading) { return; } sound.preloading = true; const video = sound.image.endsWith('.webm') || sound.type === 'video/webm'; await Promise.all([ !sound.standaloneVideo && new Promise(resolve => { const audio = new Audio(); audio.addEventListener('canplaythrough', resolve); audio.addEventListener('error', resolve); audio.src = sound.src; }), video && new Promise(resolve => { const video = document.createElement('video'); video.addEventListener('canplaythrough', resolve); video.addEventListener('error', resolve); video.src = sound.image; }) ]); sound.preloading = false; } }; /***/ }), /***/ "./src/components/position/index.js": /*!******************************************!*\ !*** ./src/components/position/index.js ***! \******************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { const selectors = __webpack_require__(/*! ../../selectors */ "./src/selectors.js"); /* eslint-disable max-statements-per-line, no-empty */ module.exports = { initialize() { // Set the header offsets for use in templates. const { top, bottom } = Player.position.getHeaderOffset(); Player.config.offsetTop = top + 'px'; Player.config.offsetBottom = bottom + 'px'; // 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(':'); +width && +height && Player.position.resize(width, height, true); +top && +left && Player.position.move(top, left); 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() { 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(e) { try { e.preventDefault(); } catch (e) { } Player._startX = (e.touches && e.touches[0] || e).clientX; Player._startY = (e.touches && e.touches[0] || e).clientY; let { width, height } = Player.container.getBoundingClientRect(); Player._startWidth = width; Player._startHeight = height; Player._startTop = Player.container.offsetTop; Player._startLeft = Player.container.offsetLeft; const dir = e.currentTarget.dataset.direction || 'se'; Player._resizeX = dir.includes('e') ? 1 : dir.includes('w') ? -1 : 0; Player._resizeY = dir.includes('s') ? 1 : dir.includes('n') ? -1 : 0; Player._resizeMoveX = dir.includes('w') ? -1 : 0; Player._resizeMoveY = dir.includes('n') ? -1 : 0; Player._resizeTarget = e.currentTarget; }, /** * Handle the user dragging the expander. */ doResize(e) { try { e.preventDefault(); } catch (e) { } const xDelta = ((e.touches && e.touches[0] || e).clientX - Player._startX) * Player._resizeX; const yDelta = ((e.touches && e.touches[0] || e).clientY - Player._startY) * Player._resizeY; const reposition = Player._resizeTarget.dataset.bypassPosition !== 'true' && (Player._resizeMoveX || Player._resizeMoveY); Player.position.resize(Player._startWidth + xDelta, Player._startHeight + yDelta, reposition || Player._resizeTarget.dataset.allowOffscreen); // If the direction is north or east then the player will need moving first. if (reposition) { Player.position.move(Player._startLeft + (xDelta * Player._resizeMoveX), Player._startTop + (yDelta * Player._resizeMoveY)); } }, /** * Handle the user releasing the expander. */ stopResize(e) { try { e.preventDefault(); } catch (e) { } const { width, height } = Player.container.getBoundingClientRect(); if (Player._resizeTarget.dataset.bypassSave !== 'true') { GM.setValue('size', width + ':' + height); if (Player._resizeMoveX || Player._resizeMoveY) { GM.setValue('position', parseInt(Player.container.style.left, 10) + ':' + parseInt(Player.container.style.top, 10)); } } }, /** * Resize the player. */ resize(width, height, allowOffscreen) { if (!Player.container || Player.config.viewStyle === 'fullscreen') { return; } const { top, bottom } = Player.position.getHeaderOffset(); // Make sure the player isn't larger than the screen, or going off screen unless allowed. height = Math.min(height, document.documentElement.clientHeight - (allowOffscreen ? (top + bottom) : Player.container.offsetTop + bottom)); width = Math.min(width, document.documentElement.clientWidth - (allowOffscreen ? 0 : Player.container.offsetLeft)); Player.container.style.width = width + 'px'; Player.container.style.height = height + 'px'; Player.controls.preventWrapping(); Player.playlist.setImageHeight(); }, /** * Handle the user grabbing the header. */ initMove(e) { if (e.target.nodeName === 'A' || e.target.closest('a') || e.target.classList.contains(`${ns}-expander`)) { return e.preventDrag = true; } try { e.preventDefault(); } catch (e) { } 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); const clientX = (e.touches && e.touches[0] || e).clientX; const clientY = (e.touches && e.touches[0] || e).clientY; Player._offsetX = clientX - Player.container.offsetLeft; Player._offsetY = clientY - Player.container.offsetTop; }, /** * Handle the user dragging the header. */ doMove(e) { try { e.preventDefault(); } catch (e) { } const clientX = (e.touches && e.touches[0] || e).clientX; const clientY = (e.touches && e.touches[0] || e).clientY; Player.position.move(clientX - Player._offsetX, clientY - Player._offsetY); }, /** * Handle the user releasing the heaer. */ stopMove(e) { try { e.preventDefault(); } catch (e) { } 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(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() { 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(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/posts/index.js": /*!***************************************!*\ !*** ./src/components/posts/index.js ***! \***************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const selectors = __webpack_require__(/*! ../../selectors */ "./src/selectors.js"); const protocolRE = /^(https?:)?\/\//; const filenameRE = /(.*?)[[({](?:audio|sound)[ =:|$](.*?)[\])}]/gi; let localCounter = 0; module.exports = { addPosts(target, postRender) { let addedSounds = false; let posts = target.classList.contains('post') ? [ target ] : target.querySelectorAll(selectors.posts); posts.forEach(post => Player.posts.addPost(post, postRender) && (addedSounds = true)); if (addedSounds && postRender && Player.container) { Player.playlist.render(); } }, addPost(post, skipRender) { try { // Ignore the style fetcher post created by this script, quoted posts, and posts with no file. let parent = post.parentElement; let parentParent = parent && parent.parentElement; if (post.classList.contains('style-fetcher') || parentParent && parentParent.id === 'qp' || parent && parent.classList.contains('noFile')) { return; } const postID = post.id.slice(selectors.postIdPrefix.length); // If there's a play or add button this post has already been parsed. Just wire up the link. let playLink = post.querySelector(`.${ns}-play-link`); let addLink = post.querySelector(`.${ns}-unfilter-link`); if (playLink || addLink) { playLink && Player.events.apply(playLink); addLink && Player.events.apply(addLink); 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 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'); if (imageMD5 === 'HO0kbeZNQqBye1CF7Tq7hg==' && post.innerHTML.includes('[futari no christmas]')) { filename = 'futari no christmas[sound=files.catbox.moe/ahvi2c.opus]'; } const { sounds, filtered } = Player.posts.getSounds(filename, imageSrc, postID, thumbSrc, imageMD5); if (sounds.length || filtered.length) { sounds.forEach(sound => Player.add(sound, skipRender)); filtered.forEach(sound => Player.filteredSounds.push(sound)); Player.posts.updateButtons(postID); filtered.length && Player.trigger('filters-applied'); } 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); } }, getSounds(filename, image, post, thumb, imageMD5, bypassVerification) { if (!filename) { return { sounds: [], filtered: [] }; } // Best quality image. For webms this has to be the thumbnail still. SAD! const imageOrThumb = image.endsWith('webm') ? thumb : image; 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, filtered }, 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, filtered }; } const sound = { src, id, title, name, post, image, imageOrThumb, filename, thumb, imageMD5, standaloneVideo }; sound.disallow = !bypassVerification && Player.disallowedSound(sound); if (!sound.disallow) { sounds.push(sound); } else if (!sound.disallow.invalid) { filtered.push(sound); } return { sounds, filtered }; }, { sounds: [], filtered: [] }); }, /** * Read all the sounds from the thread again. */ refresh() { Player.posts.addPosts(document.body); }, updateButtons(postId) { const postEl = document.getElementById(selectors.postIdPrefix + postId); if (postEl) { const linkInfo = selectors.playLink; const relative = linkInfo.relative && postEl.querySelector(linkInfo.relative); // Create/update the unfilter button, or remove it. let addLink = relative.parentNode.querySelector(`.${ns}-unfilter-link`); const allFilters = Player.posts.getFilters(postId); const hasFilter = allFilters.host.length || allFilters.image || allFilters.sound.length; if (hasFilter) { postEl.classList.add('filtered-sound'); // There is a filtered sound for the post so create/update the add link, const filtered = [ allFilters.image && 'image', allFilters.sound.length && 'sound' ].filter(Boolean).join(' and '); const hint = (allFilters.host.length > 1 ? `The hosts ${allFilters.host.join(', ')} are not allowed` : '') + (allFilters.host.length === 1 ? `The host ${allFilters.host[0]} is not allowed` : '') + (filtered ? `${allFilters.host.length ? ', and the' : 'The'} player filters disallow this ${filtered}` : '') + '. Click to allow and add to the player.'; if (addLink) { addLink.dataset.content = hint; } else { _.element('' + (linkInfo.prependText || '') + `${linkInfo.unfilterText || ''}` + (linkInfo.appendText || '') + '', relative, linkInfo.position); } } else { // There isn't a filtered so remove the add link. postEl.classList.remove('filtered-sound'); addLink && addLink.parentNode.parentNode.removeChild(addLink.parentNode); addLink && addLink.infoEl && addLink.infoEl.parentNode.removeChild(addLink.infoEl); } // Remove the play button if all sounds in the post are filtered, otherwise create it if needed. let playLink = postEl.querySelector(`.${ns}-play-link`); const addedSound = Player.sounds.find(sound => sound.post === postId); if (playLink && !addedSound) { playLink.parentNode.parentNode.removeChild(playLink.parentNode); } else if (!playLink && addedSound) { _.element('' + (linkInfo.prependText || '') + `${linkInfo.text || ''}` + (linkInfo.appendText || '') + '', relative, linkInfo.position); } } }, getFilters(postId) { return Player.filteredSounds.reduce((reason, sound) => { if (sound.post === postId) { reason.host = reason.host.concat(sound.disallow.host || []); reason.image = reason.image || sound.disallow.image; reason.sound = reason.sound.concat(sound.disallow.sound || []); } return reason; }, { host: [], image: false, sound: [] }); }, allowPost(postId) { const allowed = Player.posts.getFilters(postId); if (allowed.host.length) { Player.set('allow', Player.config.allow.concat(allowed.host)); } if (allowed.image || allowed.sound.length) { Player.set('filters', Player.config.filters.filter(filter => { return filter !== allowed.image && !allowed.sound.find(sound => filter.replace(/^(https?:)?\/\//, '') === sound); })); } } }; /***/ }), /***/ "./src/components/settings/colorpicker.js": /*!************************************************!*\ !*** ./src/components/settings/colorpicker.js ***! \************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const colorpickerTemplate = __webpack_require__(/*! ./templates/colorpicker.tpl */ "./src/components/settings/templates/colorpicker.tpl"); const HEIGHT = 200; const WIDTH = 200; module.exports = { initialize() { Player.on('menu-close', menu => menu._input && (delete menu._input._colorpicker)); }, create(e) { const parent = e.currentTarget.parentNode; const input = e.currentTarget.nodeName === 'INPUT' ? e.currentTarget : 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 rgb = Player.colorpicker.parseRGB(window.getComputedStyle(preview).backgroundColor); const colorpicker = _.element(colorpickerTemplate({ 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); }, hueMove(e) { const colorpicker = e.currentTarget.closest(`.${ns}-colorpicker`); const y = Math.max(0, e.clientY - e.currentTarget.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.currentTarget.querySelector('.position').style.top = Math.max(-3, (y - 6)) + 'px'; Player.colorpicker.updateOutput(colorpicker, true); }, satMove(e) { const colorpicker = e.currentTarget.closest(`.${ns}-colorpicker`); const saturationPosition = e.currentTarget.querySelector('.position'); const x = Math.max(0, e.clientX - e.currentTarget.getBoundingClientRect().left); const y = Math.max(0, e.clientY - e.currentTarget.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); }, inputRGBA(e) { const colorpicker = e.currentTarget.closest(`.${ns}-colorpicker`); colorpicker._colorpicker.rgb[+e.currentTarget.getAttribute('data-color')] = e.currentTarget.value; Player.colorpicker.updateOutput(colorpicker); }, updateOutput(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})`; }, apply(e) { // Update the input. const colorpicker = e.currentTarget.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(); }, parseRGB(str) { const rgbMatch = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); return [ +rgbMatch[1] || 0, +rgbMatch[2] || 0, +rgbMatch[3] || 0, isNaN(+rgbMatch[4]) ? 1 : rgbMatch[4] ]; }, hsv2rgb(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 || 1 ]; }, rgb2hsv(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 || 1 ]; }, _updatePreview(e) { const value = e.currentTarget.value; const preview = e.currentTarget.parentNode.querySelector(`.${ns}-cp-preview`); preview.style.background = value; } }; /***/ }), /***/ "./src/components/settings/hosts.js": /*!******************************************!*\ !*** ./src/components/settings/hosts.js ***! \******************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { function validURL(value) { try { new URL(value.replace(/%s/, 'sound').replace(/^(https?\/\/)?/, 'https://')); return true; } catch (err) { return false; } } module.exports = { template: __webpack_require__(/*! ./templates/host_input.tpl */ "./src/components/settings/templates/host_input.tpl"), fields: { name: 'Name', url: 'URL', responsePath: 'Response Path', responseMatch: 'Response Match', soundUrl: 'File URL Format' }, parse(newValue, hosts, e) { hosts = { ...hosts }; const container = e.currentTarget.closest(`.${ns}-host-input`); let name = container.getAttribute('data-host-name'); let host = hosts[name] = { ...hosts[name] }; const changedField = e.currentTarget.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' && newValue) && !validURL(newValue)) { 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) && validURL(container.querySelector('[name=url]').value) && (!soundUrlValue || validURL(soundUrlValue)) && (!headersValue || JSON.parse(headersValue))) { delete host.invalid; container.classList.remove('invalid'); } } catch (err) { // leave it invalid } return hosts; }, add() { let i, name = 'New Host'; // eslint-disable-next-line curly for (i = ''; Player.config.uploadHosts[`${name}${i}`]; i = ` ${++i}`); const hosts = { [`${name}${i}`]: { invalid: true, data: { file: '$file' } }, ...Player.config.uploadHosts }; Player.settings.set('uploadHosts', hosts, { bypassValidation: true, silent: true }); }, remove(e) { const hosts = Player.config.uploadHosts; const container = e.currentTarget.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 }); }, setDefault(_new, _current, e) { const selected = e.currentTarget.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; }, restoreDefaults() { Object.assign(Player.config.uploadHosts, Player.settings.findDefault('uploadHosts').default); Player.set('uploadHosts', Player.config.uploadHosts, { bypassValidation: true }); } }; /***/ }), /***/ "./src/components/settings/index.js": /*!******************************************!*\ !*** ./src/components/settings/index.js ***! \******************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const settingsConfig = __webpack_require__(/*! config */ "./src/config/index.js"); const migrations = __webpack_require__(/*! ../../migrations */ "./src/migrations.js"); const hosts = __webpack_require__(/*! ./hosts */ "./src/components/settings/hosts.js"); module.exports = { asdf: 'asdf', atRoot: [ 'set' ], public: [ 'set', 'export', 'import', 'reset', 'load' ], hosts, template: __webpack_require__(/*! ./templates/settings.tpl */ "./src/components/settings/templates/settings.tpl"), settingTemplate: __webpack_require__(/*! ./templates/setting.tpl */ "./src/components/settings/templates/setting.tpl"), inputTemplates: { checkbox: __webpack_require__(/*! ./templates/inputs/checkbox.tpl */ "./src/components/settings/templates/inputs/checkbox.tpl"), input: __webpack_require__(/*! ./templates/inputs/input.tpl */ "./src/components/settings/templates/inputs/input.tpl"), select: __webpack_require__(/*! ./templates/inputs/select.tpl */ "./src/components/settings/templates/inputs/select.tpl"), textarea: __webpack_require__(/*! ./templates/inputs/textarea.tpl */ "./src/components/settings/templates/inputs/textarea.tpl") }, changelog: 'https://github.com/rcc11/4chan-sounds-player/releases', groups: settingsConfig.reduce((groups, setting) => { if (setting.displayGroup) { groups[setting.displayGroup] || (groups[setting.displayGroup] = []); groups[setting.displayGroup].push(setting); } return groups; }, {}), async initialize() { Player.settings.view = 'Display'; // Apply the board theme as default. Player.theme.applyBoardTheme({ bypassRender: true }); // Load the config. await Player.settings.load(await GM.getValue('settings') || {}, { applyDefault: true, bypassAll: true }); // Show update notifications. if (Player.config.showUpdatedNotification && Player.config.VERSION && Player.config.VERSION !== "3.5.0") { Player.alert(`4chan Sounds Player has been updated to version ${"3.5.0"}.`); } // 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.load(value, { bypassSave: true, applyDefault: true, ignore: [ 'viewStyle' ] })); Player.on('rendered', Player.settings.setChangeListeners); }, render() { const settingsContainer = Player.$(`.${ns}-settings`); _.elementHTML(settingsContainer, Player.settings.template()); Player.settings.setChangeListeners(); }, renderSetting(settingConfig) { const settingEl = Player.$(`.${ns}-setting[data-property="${settingConfig.property}"]`); const newEl = _.element(Player.settings.settingTemplate(settingConfig), settingEl, 'beforebegin'); settingEl.parentNode.removeChild(settingEl); Player.settings.setChangeListeners(newEl); }, /** * Update a setting. */ set(property, value, { bypassAll, bypassValidation, bypassSave, bypassRender, silent, bypassStylesheet, settingConfig } = {}) { settingConfig = settingConfig || Player.settings.findDefault(property); const previous = _.get(Player.config, property); // Check if the value has actually changed. if (!bypassValidation && _.isEqual(previous, value)) { return; } // Set the new value. _.set(Player.config, property, value); // Trigger events, unless they are disabled in opts. if (!bypassAll) { !bypassStylesheet && settingConfig && settingConfig.updateCSSVars && Player.display.updateCSSVars(); !silent && Player.trigger('config', property, value, previous); !silent && Player.trigger('config:' + property, value, previous); !bypassSave && Player.settings.save(); !bypassRender && settingConfig.displayGroup && Player.settings.renderSetting(settingConfig); (!bypassRender || bypassRender === 'self') && settingConfig.dependentRender && settingConfig.dependentRender.forEach(prop => Player.settings.renderSetting(Player.settings.findDefault(prop))); } return [ previous, value ]; }, /** * Reset a setting to the default value */ reset(property, opts) { let settingConfig = Player.settings.findDefault(property); Player.set(property, settingConfig.default, { ...opts, settingConfig }); }, /** * Load a configuration object. * * @param {Object} settings Config to load * @param {Object} opts Same as Player.set, and applyDefault to reset defaults instead mixing current values. */ async load(settings, opts = {}) { if (typeof settings === 'string') { settings = JSON.parse(settings); } const changes = {}; 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) { // Mix in default. setting.mix && (value = { ...setting.default, ...(value || {}) }); const data = Player.set(setting.property, value, { bypassAll: true, settingConfig: setting }); data && (changes[setting.property] = data); } }); // Run any migrations to get up to date, and update the stored changes for event triggering. Object.entries(await Player.settings.migrate(settings.VERSION)).forEach(([ prop, [ previous, current ] ]) => { changes[prop] = [ changes[prop] ? changes[prop][1] : previous, current ]; }); // Finally, trigger events. if (!opts.bypassAll) { !opts.bypassStylesheet && Player.display.updateCSSVars(); !opts.silent && Object.entries(changes).forEach(([ prop, [ previous, current ] ]) => { Player.trigger('config', prop, current, previous); Player.trigger('config:' + prop, current, previous); }); !opts.bypassSave && Player.settings.save(); !opts.bypassRender && Player.settings.render(); } }, /** * Persist the player settings. */ save() { 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.5.0"; // Save the settings. return GM.setValue('settings', JSON.stringify(settings)); } catch (err) { Player.logError('There was an error saving the sound player settings.', err); } }, /** * Run migrations when the player is updated. */ async migrate(fromVersion) { // Fall out if the player hasn't updated. if (!fromVersion || fromVersion === "3.5.0") { return {}; } const changes = {}; 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); Object.entries(await mig.run()).forEach(([ prop, [ current, previous ] ]) => { changes[prop] = [ current, changes[prop] ? changes[prop][1] : previous ]; }); } catch (err) { console.error(err); } } } return changes; }, /** * Compare two semver strings. */ compareVersions(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(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, actions: null, settings: null, description: null, ...subSetting, isSubSetting: true }); } return false; }); return settingConfig || { property }; }, /** * Toggle whether the player or settings are displayed. */ toggle(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); } }, showGroup(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'); }, async import() { const fileInput = _.element(''); const _import = async () => { let config; try { config = await (await fetch(URL.createObjectURL(fileInput.files[0]))).json(); } catch (err) { Player.logError(`Expected a JSON config file and got ${fileInput.files[0].type}.`, err, 'warning'); } fileInput.removeEventListener('change', _import); Player.settings.load(config); }; fileInput.addEventListener('change', _import); fileInput.click(); }, async export(e) { // Use the saved settings to only export non-default user settings. Shift click exports everything for testing. const settings = e && e.shiftKey ? JSON.stringify(Player.config, null, 4) : await GM.getValue('settings') || '{}'; const blob = new Blob([ settings ], { type: 'application/json' }); const a = _.element(``); a.click(); URL.revokeObjectURL(a.href); }, setChangeListeners(target) { const settingsContainer = target || Player.$(`.${ns}-settings`); settingsContainer.querySelectorAll(`.${ns}-settings input, .${ns}-settings textarea`).forEach(el => { el.addEventListener('focusout', Player.settings.handleChange); }); settingsContainer.querySelectorAll(`.${ns}-settings input[type=checkbox], .${ns}-settings select`).forEach(el => { el.addEventListener('change', Player.settings.handleChange); }); }, /** * Handle the user making a change in the settings view. */ handleChange(e) { try { const input = e.currentTarget; 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); } // 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: 'self', 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(e) { e.preventDefault(); if (e.key === 'Shift' || e.key === 'Control' || e.key === 'Meta') { return; } e.currentTarget.value = e.which === 8 || e.key.toLowerCase() === 'backspace' ? '' : Player.hotkeys.stringifyKey(e); } }; /***/ }), /***/ "./src/components/theme/index.js": /*!***************************************!*\ !*** ./src/components/theme/index.js ***! \***************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const selectors = __webpack_require__(/*! ../../selectors */ "./src/selectors.js"); const settingsConfig = __webpack_require__(/*! config */ "./src/config/index.js"); const saveMenuTemplate = __webpack_require__(/*! ./templates/save_theme_menu.tpl */ "./src/components/theme/templates/save_theme_menu.tpl"); module.exports = { public: [ 'switch', 'next', 'previous' ], savedThemesTemplate: __webpack_require__(/*! ./templates/saved_themes.tpl */ "./src/components/theme/templates/saved_themes.tpl"), themeKeybindsTemplate: __webpack_require__(/*! ./templates/theme_keybinds.tpl */ "./src/components/theme/templates/theme_keybinds.tpl"), async initialize() { // Create the user stylesheet and update it when dependent config values are changed. Player.theme.render(); Player.userTemplate.maintain(Player.theme, 'customCSS'); Player.theme.validateOrder(); }, render() { Player.userStylesheet = Player.userStylesheet || _.element('', document.head); Player.userStylesheet.innerHTML = Player.userTemplate.build({ template: '/* Sounds Player User CSS */\n\n' + Player.config.customCSS, sound: Player.playing, ignoreButtons: true, ignoreDisplayBlocks: true, ignoreSoundName: true, ignoreVersion: true }); }, forceBoardTheme() { Player.theme.applyBoardTheme({ force: true }); Player.settings.save(); }, /** * Get colors from the board theme. */ applyBoardTheme(opts = {}) { // Create a reply element to gather the style from const div = _.element(`
    `, document.body); const style = document.defaultView.getComputedStyle(div); // Make sure the style is loaded. // TODO: This sucks. Should observe the stylesheets for changes to make it work. // That would also make theme changes apply without a reload. if (style.backgroundColor === 'rgba(0, 0, 0, 0)') { return setTimeout(Player.display.applyBoardTheme, 0); } Object.assign(style, { page_background: window.getComputedStyle(document.body).backgroundColor }); // Apply the computed style to the color config. const colorSettingMap = { 'colors.text': style.color, 'colors.background': style.backgroundColor, 'colors.odd_row': style.backgroundColor, 'colors.border': style.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 ? style.backgroundColor : style.borderBottomColor, // Set this for use in custom css and templates 'colors.page_background': window.getComputedStyle(document.body).backgroundColor, // Playing row is a more saturated and brighter odd row. 'colors.playing': (() => { const oddRowHSV = Player.colorpicker.rgb2hsv(...Player.colorpicker.parseRGB(style.backgroundColor)); const playingRGB = Player.colorpicker.hsv2rgb( oddRowHSV[0], Math.min(1, oddRowHSV[1] + 0.25), Math.min(1, oddRowHSV[2] + 0.15) ); return `rgb(${playingRGB[0]}, ${playingRGB[1]}, ${playingRGB[2]})`; })() }; settingsConfig.find(s => s.property === 'colors').settings.forEach(setting => { const updateConfig = opts.force || (setting.default === _.get(Player.config, setting.property)); colorSettingMap[setting.property] && (setting.default = colorSettingMap[setting.property]); updateConfig && Player.set(setting.property, setting.default, { bypassSave: true, bypassRender: true, bypassStylesheet: true }); }); // Clean up the element. document.body.removeChild(div); if (!opts.bypassRender) { // Updated the stylesheet. Player.display.updateCSSVars(); // Re-render the settings. Player.settings.render(); } }, /** * Switch to the next theme, wrapping round to the beginning. */ next() { const order = [ 'Default' ].concat(Player.config.savedThemesOrder); const cIndex = order.indexOf(Player.config.selectedTheme); const next = order[(cIndex + order.length + 1) % order.length]; Player.theme.switch(next); }, /** * Switch to the previous theme, wrapping round to the end. */ previous() { const order = [ 'Default' ].concat(Player.config.savedThemesOrder); const cIndex = order.indexOf(Player.config.selectedTheme); const previous = order[(cIndex + order.length - 1) % order.length]; Player.theme.switch(previous); }, /** * Switch theme. * @param {String} name The name of the theme to switch to. * @param {Object} opts Options passed to player.load */ switch(name) { if (name !== 'Default' && !Player.config.savedThemes[name]) { return Player.logError(`Theme '${name}' does not exist.`, null, 'warning'); } Player.set('selectedTheme', name); if (name === 'Default') { [ 'headerTemplate', 'footerTemplate', 'rowTemplate', 'chanXTemplate', 'customCSS' ].forEach(prop => { Player.settings.reset(prop, { bypassRender: true, bypassStylesheet: true }); }); Player.settings.render(); Player.display.updateCSSVars(); } else { Player.settings.load(Player.config.savedThemes[name]); } }, /** * Make sure the saved themes order contains all and only the saved themes, without duplicates. */ validateOrder() { const order = Player.config.savedThemesOrder; let _i; Player.config.savedThemesOrder = order .concat(Object.keys(Player.config.savedThemes)) .filter((name, i) => Player.config.savedThemes[name] && (_i = order.indexOf(name), _i === -1 || _i === i)); }, parseSwitch(newValue, bindings, e) { bindings = [ ...bindings ]; const themeName = e.currentTarget.parentNode.dataset.name; if (themeName !== 'Default' && !Player.config.savedThemes[themeName]) { Player.logError(`No theme named '${themeName}'.`, null, 'warning'); } const keyDef = Player.hotkeys.parseKey(newValue); if (!keyDef.key) { bindings = bindings.filter(def => def.themeName !== themeName); } else { let binding = bindings.find(def => def.themeName === themeName); binding || bindings.push(binding = { themeName }); Object.assign(binding, keyDef); } return bindings; }, handleSwitch(e) { Player.theme.switch(e._binding.themeName); }, moveUp: e => Player.theme._swapOrder(e, -1), moveDown: e => Player.theme._swapOrder(e, 1), _swapOrder(e, dir) { const name = e.currentTarget.closest('[data-theme]').dataset.theme; const order = Player.config.savedThemesOrder; const i = order.indexOf(name); if (i + dir >= 0 && i + dir < order.length) { [ order[i], order[i + dir] ] = [ order[i + dir], order[i] ]; Player.$(`[data-theme="${name}"]`).style.order = i + dir; Player.$(`[data-theme="${order[i]}"]`).style.order = i; Player.settings.set('savedThemes', Player.config.savedThemes, { bypassValidation: true, bypassRender: true }); } }, remove(e) { const themes = Player.config.savedThemes; const row = e.currentTarget.closest('[data-theme]'); const name = row.dataset.theme; // Can't delete the default. It's not actually a stored theme. if (name === 'Default') { return Player.logError('Cannot delete the default theme. You can instead overwrite it.', null, 'warning'); } // For default themes set null so we know to not include them. if (Player.settings.findDefault('savedThemes').default[name]) { themes[name] = null; } else { delete themes[name]; } Player.config.savedThemesOrder = Player.config.savedThemesOrder.filter(_name => _name !== name); // Remove the row row.parentNode.removeChild(row); Player.settings.set('savedThemes', themes, { bypassValidation: true, bypassRender: true }); // Remove hotkey binding const bindingIndex = Player.config.hotkey_bindings.switchTheme.find(def => def.themeName === name); if (bindingIndex) { Player.set('hotkey_bindings.switchTheme', Player.config.hotkey_bindings.switchTheme.splice(bindingIndex, 1), { bypassValidation: true }); } }, restoreDefaults() { Object.assign(Player.config.savedThemes, Player.settings.findDefault('savedThemes').default); Player.theme.validateOrder(); Player.set('savedThemes', Player.config.savedThemes, { bypassValidation: true }); }, showSaveOptions(e) { const open = Player.$(`.${ns}-theme-save-options`); if (open) { return Player.container.removeChild(open); } const el = _.element(saveMenuTemplate({ settingsConfig }), Player.container); Player.position.showRelativeTo(el, e.currentTarget); Player.$(`.${ns}-save-theme-name`).focus(); }, toggleSaveFields() { Player.$(`.${ns}-theme-save-options`).classList.toggle('fields-collapsed'); Player.position.showRelativeTo(Player.$(`.${ns}-theme-save-options`), Player.$('[\\@click^="theme.showSaveOptions"]')); }, toggleSaveButtonText(e) { Player.$(`.${ns}-save-theme`).innerHTML = Player.config.savedThemes[e.currentTarget.value] ? 'Update' : 'Create'; }, save() { const name = Player.$(`.${ns}-save-theme-name`).value; if (!name) { return Player.logError('A name is required to save a theme.', null, 'warning'); } const checked = Player.$all(`.${ns}-theme-save-options input:checked`); const data = [ ...checked ].reduce((data, el) => _.set(data, el.value, _.get(Player.config, el.value)), {}); Player.config.savedThemes[name] = data; Player.config.savedThemesOrder.indexOf(name) === -1 && Player.config.savedThemesOrder.push(name); Player.set('savedThemes', Player.config.savedThemes, { bypassValidation: true }); Player.container.removeChild(Player.$(`.${ns}-theme-save-options`)); } }; /***/ }), /***/ "./src/components/theme/themes.js": /*!****************************************!*\ !*** ./src/components/theme/themes.js ***! \****************************************/ /***/ ((module) => { module.exports = { 'Fixed Playlist': { chanXControls: 'always', headerTemplate: '
    \nrepeat-button shuffle-button hover-images-button playlist-button\nsound-title-marquee\nview-menu-button add-button theme-menu-button close-button', rowTemplate: 'sound-title\np:{}\n', customCSS: '/* Fix the player to the right at full height (minus the 4chan X header) */\n#fcsp-container[data-view-style="playlist"] {\n\t/* Fixed width *\\/\n\twidth: 25rem !important;/**/\n\theight: auto !important;\n\ttop: $config[offsetTop] !important;\n\tright: 0px !important;\n\tbottom: $config[offsetBottom] !important;\n\tleft: auto !important;\n\theight: auto !important;\n\tmax-height: calc(100% - 24px);\n\tbackground: none !important;\n\tborder: none !important;\n}\n\n/* Hide things when the playlist is open */\n/* Hide the image with the playlist open, unless it\'s a webm.\n * To show gifs as well change playing-video to playing-animated. */\n#fcsp-container[data-view-style="playlist"]:not(.playing-video) .fcsp-image-link,/**/ \n#fcsp-container[data-view-style="playlist"] .fcsp-controls,\n#fcsp-container[data-view-style="playlist"] .fcsp-hover-image,\n#fcsp-container[data-view-style="playlist"] .fcsp-footer,\n#fcsp-container[data-view-style="fullscreen"] .fcsp-row-thumb,\n#fcsp-container[data-view-style="fullscreen"] .fcsp-row-image {\n\tdisplay: none !important;\n}\n/* Header is shown with adjustments to handle the changed container style */\n/* Opacity and absolute position are used to auto hide the header */\n#fcsp-container[data-view-style="playlist"] .fcsp-header {\n\tposition: absolute !important;\n\topacity: 0;\n\tz-index: 9;\n\tcursor: inherit;\n\tbackground: $config[colors.background];\n\tborder-width: 0 1px 1px 0;\n\ttransition: all .3s ease;\n}\n#fcsp-container[data-view-style="playlist"] .fcsp-header:hover {\n\topacity: 1;\n}\n\n/* Don\'t show a scrollbar for the playlist for aesthetic reasons */\n#fcsp-container[data-view-style="playlist"] .fcsp-under-image {\n\tscrollbar-width: none;\n}\n#fcsp-container[data-view-style="playlist"] .fcsp-under-image::-webkit-scrollbar {\n\tdisplay: none;\n}\n\n/* Chunky playlist items, with no background and a squared thumbnail image. */\n#fcsp-container[data-view-style="playlist"] .fcsp-list-item {\n\tbackground: none !important;\n\theight: auto !important;\n\tline-height: initial;\n\ttext-align: right;\n\talign-items: center;\n\ttransition: all .5s ease;\n\tfont-size: 1rem;\n\tcolor: $config[colors.text];\n}\n#fcsp-container[data-view-style="playlist"] .fcsp-row-thumb, #fcsp-container[data-view-style="playlist"] .fcsp-row-image {\n\theight: 3rem;\n\twidth: 3rem;\n\tobject-fit: cover;\n\ttransition: all .5s ease;\n}\n/* Show a gradient background and increase the size of list items when you hover over them. */\n#fcsp-container[data-view-style="playlist"] .fcsp-list-item:hover {\n\tfont-size: 1.5rem;\n\tfont-weight: bold;\n\tbackground-image: radial-gradient(circle at -50%, #0000 70%, $config[colors.odd_row]) !important;\n\tcolor: $config[colors.background];\n\t-webkit-text-stroke: 1px black;\n}\n#fcsp-container[data-view-style="playlist"] .fcsp-list-item:hover .fcsp-row-thumb {\n\theight: 4rem;\n\twidth: 4rem;\n}\n#fcsp-container[data-view-style="playlist"] .fcsp-row-image {\n\theight: 7rem;\n\twidth: 7rem;\n}\n/* Add a gradient background to the playing item, make the text bigger, and style the text. */\n#fcsp-container[data-view-style="playlist"] .fcsp-list-item.playing {\n\tbackground-image: radial-gradient(circle at -50%, #0000 70%, $config[colors.playing]) !important;\n\tpadding-left: 2px;\n\tfont-weight: bold;\n\tfont-size: 1.5rem;\n\tcolor: $config[colors.page_background];\n\t-webkit-text-stroke: 1px black;\n\ttext-shadow: 0 0 2px $config[colors.border];\n}\n/* Swap the thumb image with the full image for the playing item. */\n#fcsp-container[data-view-style="playlist"] .fcsp-list-item:not(.playing) .fcsp-row-image {\n\tdisplay: none;\n}\n#fcsp-container[data-view-style="playlist"] .fcsp-list-item.playing .fcsp-row-thumb {\n\tdisplay: none;\n}\n/* Same gradient background style for dragging items. */\n#fcsp-container[data-view-style="playlist"] .fcsp-dragging {\n\tbackground-image: radial-gradient(circle at -50%, #0000 70%, $config[colors.dragging]) !important\n}\n\n/* Add a resizer to the left of the header when the playlist is open. */\n.playlist-resizer {\n\topacity: 1 !important;\n\tmargin: 0 !important;\n\twidth: .25rem !important;\n\theight: auto !important;\n\tbackground-color: $config[colors.border] !important;\n\tposition: absolute;\n\tleft: -.25rem !important;\n\ttop: 0 !important;\n\tbottom: 0 !important;\n\tborder-radius: 100% 0 0 100%;\n\tcursor: ew-resize !important;\n\tdisplay: none;\n\ttransition: all .5s ease;\n}\n.playlist-resizer:hover {\n\twidth: .4rem !important;\n\tleft: -.4rem !important;\n}\n#fcsp-container[data-view-style="playlist"] .playlist-resizer {\n\tdisplay: block;\n}\n/* Hide the default resizers */\n#fcsp-container[data-view-style="playlist"] .fcsp-expander:not(.playlist-resizer) {\n\tdisplay: none;\n}\n' } }; /***/ }), /***/ "./src/components/threads/index.js": /*!*****************************************!*\ !*** ./src/components/threads/index.js ***! \*****************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.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 = { template: __webpack_require__(/*! ./templates/threads.tpl */ "./src/components/threads/templates/threads.tpl"), boardsTemplate: __webpack_require__(/*! ./templates/boards.tpl */ "./src/components/threads/templates/boards.tpl"), listsTemplate: __webpack_require__(/*! ./templates/list.tpl */ "./src/components/threads/templates/list.tpl"), boardList: null, soundThreads: null, displayThreads: {}, selectedBoards: Board ? [ Board ] : [ 'a' ], showAllBoards: false, async initialize() { 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() { if (Player.container && Player.config.viewStyle === 'threads' && Player.threads.boardList === null) { Player.threads.fetchBoards(true); Player.off('show', Player.threads._initialFetch); Player.off('view', Player.threads._initialFetch); } }, render() { if (Player.container) { _.elementHTML(Player.$(`.${ns}-threads`), Player.threads.template()); Player.threads.afterRender(); } }, /** * Render the threads and apply the board styling after the view is rendered. */ afterRender() { const threadList = Player.$(`.${ns}-thread-list`); if (threadList) { const bodyStyle = document.defaultView.getComputedStyle(document.body); threadList.style.background = bodyStyle.backgroundColor; threadList.style.backgroundImage = bodyStyle.backgroundImage; threadList.style.backgroundRepeat = bodyStyle.backgroundRepeat; threadList.style.backgroundPosition = bodyStyle.backgroundPosition; } Player.threads.renderThreads(); }, /** * Render just the threads. */ renderThreads() { if (!Player.threads.hasParser || Player.config.threadsViewStyle === 'table') { _.elementHTML(Player.$(`.${ns}-threads-body`), Player.threads.listsTemplate()); } 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(`
    ${boardTitle}
    `, 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('
    ', 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() { _.elementHTML(Player.$(`.${ns}-thread-board-list`), Player.threads.boardsTemplate()); }, /** * Toggle the threads view. */ toggle() { if (Player.config.viewStyle === 'threads') { Player.playlist.restore(); } else { Player.display.setViewStyle('threads'); } }, /** * Switch between showing just the selected boards and all boards. */ toggleBoardList() { 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. */ async toggleBoard(board, selected) { 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. */ async fetchBoards(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. */ async fetch() { 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 } = Player.posts.getSounds(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(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(); } }; /***/ }), /***/ "./src/components/tools/create.js": /*!****************************************!*\ !*** ./src/components/tools/create.js ***! \****************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); // Seems to be the cut off point for file names const maxFilenameLength = 218; const completeTemplate = __webpack_require__(/*! ./templates/create-complete.tpl */ "./src/components/tools/templates/create-complete.tpl"); const hostsTemplate = __webpack_require__(/*! ./templates/hosts-select.tpl */ "./src/components/tools/templates/hosts-select.tpl"); /** * This component is mixed into tools so these function are under `Player.tools`. */ const createTool = module.exports = { _uploadIdx: 0, createTemplate: __webpack_require__(/*! ./templates/create.tpl */ "./src/components/tools/templates/create.tpl"), createHostsTemplate: hostsTemplate, createCompleteTemplate: completeTemplate, /** * Update the view when the hosts are updated. */ initialize() { Player.on('config:uploadHosts', () => Player.$(`.${ns}-create-hosts-container`).innerHTML = hostsTemplate()); Player.on('config:defaultUploadHost', newValue => Player.$(`.${ns}-create-sound-host`).value = newValue); Player.on('rendered', createTool.afterRender); }, /** * Store references to various elements. */ afterRender() { createTool.status = Player.$(`.${ns}-create-sound-status`); Player.tools.imgInput = Player.$(`.${ns}-create-sound-img`); Player.tools.sndInput = Player.$(`.${ns}-create-sound-snd`); }, /** * Show/hide the "Use webm" checkbox when an image is selected. */ async handleImageSelect(e) { const input = e && e.currentTarget || Player.tools.imgInput; const image = input.files[0]; let placeholder = image.name.replace(/\.[^/.]+$/, ''); 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(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 && (_.elementHTML(fileList, files.length < 2 ? '' : files.map((file, i) => `
    ${file.name}
    ${Icons.close}
    ` ).join(''))); }, /** * Handle a file being removed from a multi input */ handleFileRemove(e) { const idx = +e.currentTarget.getAttribute('data-idx'); const input = e.currentTarget.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(e) { const sound = Player.tools.sndInput; const image = Player.tools.imgInput; Player.tools.handleFileSelect(sound, e.currentTarget.checked && [ image.files[0] ]); }, toggleSoundInput(type) { const showURL = 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(e) { 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(); // Make sure sound file input is shown if a sound file is dropped if (input === Player.tools.sndInput && Player.tools.useSoundURL) { Player.tools.toggleSoundInput('file'); } } }); return false; }, /** * Handle the create button. * Extracts video/audio if required, uploads the sound, and creates an image file names with [sound=url]. */ async handleCreate() { // Revoke the URL for an existing created image. Player.tools._createdImageURL && URL.revokeObjectURL(Player.tools._createdImageURL); Player.tools._createdImage = null; createTool.status.style.display = 'block'; createTool.status.innerHTML = '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'); } // No audio allowed for the "image" webm. if (image.type.startsWith('video') && await Player.tools.hasAudio(image)) { createTool.status.innerHTML += '
    Audio not allowed for the image webm.' + '
    Remove the audio from the webm and try again.'; throw new PlayerError('Audio not allowed for the image webm.', 'warning'); } 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. 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'); } } 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]; // Keep track of the create image and a url to it. Player.tools._createdImage = new File([ image ], filename + '.' + ext, { type: image.type }); Player.tools._createdImageURL = URL.createObjectURL(Player.tools._createdImage); // Complete! with some action links _.element(completeTemplate(), createTool.status); } catch (err) { createTool.status.innerHTML += '
    Failed! ' + (err instanceof PlayerError ? err.reason : ''); Player.logError('Failed to create sound image', err); } Player.$(`.${ns}-create-button`).disabled = false; }, hasAudio(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; }); }, /** * Upload the sound file and return a link to it. */ async postFile(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]); } }); createTool.status.innerHTML += `
    Uploading ${file.name}`; 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).trim(); Player.$(`.${ns}-upload-status-${idx}`).innerHTML = `Uploaded ${file.name} to ${uploadedUrl}`; 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() { Player.playlist.addFromFiles([ Player.tools._createdImage ]); }, /** * Open the QR window and add the created sound image to it. */ addCreatedToQR() { if (!is4chan) { return; } // 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) { 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(); } } }; /***/ }), /***/ "./src/components/tools/download.js": /*!******************************************!*\ !*** ./src/components/tools/download.js ***! \******************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const progressBarsTemplate = __webpack_require__(/*! ./templates/download-progress.tpl */ "./src/components/tools/templates/download-progress.tpl"); const get = (src, opts) => { let xhr; // Wrap so aborting rejects. let p = new Promise((resolve, reject) => { xhr = GM.xmlHttpRequest({ method: 'GET', url: src, responseType: 'blob', onload: response => resolve(response.response), onerror: response => reject(response), onabort: response => { response.aborted = true; reject(response); }, ...(opts || {}) }); }); if (opts && opts.catch) { p = p.catch(opts.catch); } p.abort = xhr.abort; return p; }; /** * This component is mixed into tools so these function are under `Player.tools`. */ const downloadTool = module.exports = { downloadTemplate: __webpack_require__(/*! ./templates/download.tpl */ "./src/components/tools/templates/download.tpl"), _downloading: null, /** * Update the view when the hosts are updated. */ initialize() { Player.on('rendered', downloadTool.afterRender); }, /** * Store references to various elements. */ afterRender() { downloadTool.resetDownloadButtons(); }, async _handleDownloadCancel() { if (Player.tools._downloading) { Player.tools._downloadAllCanceled = true; Player.tools._downloading.forEach(dls => dls.forEach(dl => dl && dl.abort())); } }, async _handleDownload(e) { Player.tools._downloadAllCanceled = false; e.currentTarget.style.display = 'none'; Player.$(`.${ns}-download-all-cancel`).style.display = null; await Player.tools.downloadThread({ includeImages: Player.$('.download-all-images').checked, includeSounds: Player.$('.download-all-audio').checked, ignoreDownloaded: Player.$('.download-all-ignore-downloaded').checked, maxSounds: +Player.$('.download-all-max-sounds').value || 0, concurrency: Math.max(1, +Player.$('.download-all-concurrency').value || 1), compression: Math.max(0, Math.min(+Player.$('.download-all-compression').value || 0, 9)), status: Player.$(`.${ns}-download-all-status`) }).catch(() => { /* it's logged */ }); Player.tools.resetDownloadButtons(); }, resetDownloadButtons() { Player.$(`.${ns}-download-all-start`).style.display = Player.tools._downloading ? 'none' : null; Player.$(`.${ns}-download-all-cancel`).style.display = Player.tools._downloading ? null : 'none'; Player.$(`.${ns}-download-all-save`).style.display = Player.tools.threadDownloadBlob ? null : 'none'; Player.$(`.${ns}-download-all-clear`).style.display = Player.tools.threadDownloadBlob ? null : 'none'; Player.$(`.${ns}-ignore-downloaded`).style.display = Player.sounds.some(s => s.downloaded) ? null : 'none'; }, /** * Trigger a download for a file using GM.xmlHttpRequest to avoid cors issues. * * @param {String} src URL of the field to download. * @param {String} name Name to save the file as. */ async download(src, name) { try { const blob = await get(src); const a = _.element(``); a.click(); URL.revokeObjectURL(a.href); } catch (err) { Player.logError('There was an error downloading.', err, 'warning'); } }, /** * Download the images and/or sounds in the thread as zip file. * * @param {Boolean} includeImages Whether images should be included in the download. * @param {Boolean} includeSounds Whether audio files should be included in the download. * @param {Boolean} ignoreDownloaded Whether sounds previously downloaded should be omitted from the download. * @param {Boolean} maxSounds The maximum number of sounds to download. * @param {Boolean} concurrency How many sounds can be download at the same time. * @param {Boolean} compression Compression level. * @param {Element} [status] Element in which to display the ongoing status of the download. */ async downloadThread({ includeImages, includeSounds, ignoreDownloaded, maxSounds, concurrency, compression, status }) { const zip = new JSZip(); !(maxSounds > 0) && (maxSounds = Infinity); const toDownload = Player.sounds.filter(s => s.post && (!ignoreDownloaded || !s.downloaded)).slice(0, maxSounds); const count = toDownload.length; status && (status.style.display = 'block'); if (!count || !includeImages && !includeSounds) { return status && (status.innerHTML = 'Nothing to download.'); } Player.tools._downloading = []; status && (status.innerHTML = `Downloading ${count} sound images.

    This may take a while. You can leave it running in the background, but if you background the tab your browser will slow it down. You'll be prompted to download the zip file once complete.

    `); const elementsArr = new Array(concurrency).fill(0).map(() => { // Show currently downloading files with progress bars. const el = status && _.element(progressBarsTemplate({ includeSounds, includeImages }), status); const dlRef = []; Player.tools._downloading.push(dlRef); // Allow each download to be canceled individually. In case there's a huge download you don't want to include. el && (el.querySelector(`.${ns}-cancel-download`).onclick = () => dlRef.forEach(dl => dl && dl.abort())); return { dlRef, el, status: el && el.querySelector(`.${ns}-current-status`), image: el && el.querySelector(`.${ns}-image-bar`), sound: el && el.querySelector(`.${ns}-sound-bar`) }; }); let running = 0; // Download arg builder. Update progress bars, and catch errors to log and continue. const getArgs = (data, sound, type) => ({ responseType: 'arraybuffer', onprogress: data[type] && (rsp => data[type].style.width = ((rsp.loaded / rsp.total) * 100) + '%'), catch: err => { if (err.aborted) { return 'aborted'; } if (!err.aborted && !Player.tools._downloadAllCanceled) { console.error('[4chan sounds player] Download failed', err); status && _.element(`

    Failed to download ${sound.title} ${type}!

    `, elementsArr[0].el, 'beforebegin'); } } }); await Promise.all(elementsArr.map(async function downloadNext(data) { const sound = toDownload.shift(); // Fall out if all downlads were canceled. if (!sound || Player.tools._downloadAllCanceled) { data.el && status.removeChild(data.el); return; } const i = ++running; // Show the name and reset the progress bars. if (data.el) { data.status.textContent = `${i} / ${count}: ${sound.title}`; data.image.style.width = data.sound.style.width = '0'; } // Create a folder per post if images and sounds are being downloaded. const prefix = includeImages && includeSounds ? sound.post + '/' : ''; // Download image and sound as selected. const [ imageRsp, soundRsp ] = await Promise.all([ data.dlRef[0] = includeImages && get(sound.image, getArgs(data, sound, 'image')), data.dlRef[1] = includeSounds && get(sound.src, getArgs(data, sound, 'sound')) ]); // No post-handling if the whole download was canceled. if (!Player.tools._downloadAllCanceled) { if (imageRsp === 'aborted' || soundRsp === 'aborted') { // Show which sounds were individually aborted. status && _.element(`

    Skipped ${sound.title}.

    `, elementsArr[0].el, 'beforebegin'); } else { // Add the downloaded files to the zip. imageRsp && zip.file(`${prefix}${sound.filename}`, imageRsp); soundRsp && zip.file(`${prefix}${encodeURIComponent(sound.src)}`, soundRsp); // Flag the sound as downloaded. sound.downloaded = true; } } // Move on to the next sound. await downloadNext(data); })); // Show where we canceled at, if we did cancel. Player.tools._downloadAllCanceled && _.element(`Canceled at ${running} / ${count}.`, status); // Generate the zip file. const zipProgress = status && _.element('
    Generating zip file...
    ', status); try { const zipOptions = { type: 'blob', compression: compression ? 'DEFLATE' : 'STORE', compressionOptions: { level: compression } }; Player.tools.threadDownloadBlob = await zip.generateAsync(zipOptions, metadata => { status && (zipProgress.textContent = `Generating zip file (${metadata.percent.toFixed(2)}%)...`); }); // Update the display and prompt to download. status && _.element('Complete!', status); Player.tools.saveThreadDownload(); } catch (err) { console.error('[4chan sounds player] Failed to generate zip', err); status && (zipProgress.textContent = 'Failed to generate zip file!'); } Player.tools._downloading = null; Player.tools.resetDownloadButtons(); }, saveThreadDownload() { const threadNum = Thread || '-'; const a = _.element(``); a.click(); URL.revokeObjectURL(a.href); }, clearDownloadBlob() { delete Player.tools.threadDownloadBlob; Player.tools.resetDownloadButtons(); } }; /***/ }), /***/ "./src/components/tools/index.js": /*!***************************************!*\ !*** ./src/components/tools/index.js ***! \***************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const createTool = __webpack_require__(/*! ./create */ "./src/components/tools/create.js"); const downloadTool = __webpack_require__(/*! ./download */ "./src/components/tools/download.js"); module.exports = { template: __webpack_require__(/*! ./templates/tools.tpl */ "./src/components/tools/templates/tools.tpl"), ...createTool, ...downloadTool, initialize() { createTool.initialize(); downloadTool.initialize(); }, render() { _.elementHTML(Player.$(`.${ns}-tools`).innerHTML, Player.tools.template()); createTool.afterRender(); downloadTool.afterRender(); }, toggle() { if (Player.config.viewStyle === 'tools') { Player.playlist.restore(); } else { Player.display.setViewStyle('tools'); } }, /** * Encode the decoded input. */ handleDecoded(e) { Player.$(`.${ns}-encoded-input`).value = encodeURIComponent(e.currentTarget.value); }, /** * Decode the encoded input. */ handleEncoded(e) { Player.$(`.${ns}-decoded-input`).value = decodeURIComponent(e.currentTarget.value); } }; /***/ }), /***/ "./src/components/user-template/buttons.js": /*!*************************************************!*\ !*** ./src/components/user-template/buttons.js ***! \*************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const { postIdPrefix } = __webpack_require__(/*! ../../selectors */ "./src/selectors.js"); module.exports = [ { property: 'repeat', tplName: 'repeat', action: 'playlist.toggleRepeat', actionMods: '.prevent', 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', action: 'playlist.toggleShuffle', actionMods: '.prevent', values: { true: { attrs: [ 'title="Shuffled"' ], icon: Icons.shuffle }, false: { attrs: [ 'title="Ordered"' ], class: 'muted', icon: Icons.shuffle } } }, { property: 'viewStyle', tplName: 'playlist', action: 'playlist.toggleView', 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', action: 'playlist.toggleHoverImages', values: { true: { attrs: [ 'title="Hover Images Enabled"' ], icon: Icons.image }, false: { attrs: [ 'title="Hover Images Disabled"' ], class: 'muted', icon: Icons.image } } }, { tplName: 'add', action: 'playlist.selectLocalFiles', actionMods: '.prevent', icon: Icons.plus, attrs: [ 'title="Add local files"' ] }, { tplName: 'reload', action: 'posts.refresh', actionMods: '.prevent', icon: Icons.reboot, attrs: [ 'title="Reload the playlist"' ] }, { property: 'viewStyle', tplName: 'settings', action: 'settings.toggle()', actionMods: '.prevent', icon: Icons.gear, attrs: [ 'title="Settings"' ], values: { default: { class: 'muted' }, settings: { } } }, { property: 'viewStyle', tplName: 'threads', action: 'threads.toggle', actionMods: '.prevent', icon: Icons.search, attrs: [ 'title="Threads"' ], values: { default: { class: 'muted' }, threads: { } } }, { property: 'viewStyle', tplName: 'tools', action: 'tools.toggle', actionMods: '.prevent', icon: Icons.tools, attrs: [ 'title="Tools"' ], values: { default: { class: 'muted' }, tools: { } } }, { tplName: 'close', action: 'hide', actionMods: '.prevent', icon: Icons.close, attrs: [ 'title="Hide the player"' ] }, { tplName: 'playing', requireSound: true, action: 'playlist.scrollToPlaying("center")', actionMods: '.prevent', icon: Icons.musicNoteList, attrs: [ 'title="Scroll the playlist to the 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, icon: Icons.soundwave, attrs: data => [ `href=${data.sound.src}`, 'title="Open the sound in a new tab"', 'target="_blank"' ] }, { tplName: /dl-(image|sound)/, requireSound: true, action: data => { const src = data.sound[data.tplNameMatch[1] === 'image' ? 'image' : 'src']; const name = data.sound[data.tplNameMatch[1] === 'image' ? 'filename' : 'name'] || ''; return `tools.download("${_.escAttr(src, true)}", "${_.escAttr(name, true)}")`; }, actionMods: '.prevent', icon: data => data.tplNameMatch[1] === 'image' ? Icons.fileEarmarkImage : Icons.fileEarmarkMusic, attrs: data => [ `title="${data.tplNameMatch[1] === 'image' ? 'Download the image with the original filename' : 'Download the sound'}"` ] }, { tplName: /filter-(image|sound)/, requireSound: true, action: data => `playlist.addFilter("${data.tplNameMatch[1] === 'image' ? data.sound.imageMD5 : data.sound.src.replace(/^(https?:)?\/\//, '')}")`, actionMods: '.prevent', icon: Icons.filter, showIf: data => data.tplNameMatch[1] === 'sound' || data.sound.imageMD5, attrs: data => [ `title="Add the ${data.tplNameMatch[1] === 'image' ? 'image MD5' : 'sound URL'} to the filters."`, ] }, { tplName: 'remove', requireSound: true, action: data => `remove("${data.sound.id}")`, icon: Icons.trash, attrs: data => [ 'title="Filter the image."', `data-id="${data.sound.id}"` ] }, { tplName: 'menu', requireSound: true, class: `${ns}-item-menu-button`, action: data => `playlist.handleItemMenu($event, "${data.sound.id}")`, actionMods: '.prevent.stop', icon: Icons.chevronDown }, { tplName: 'view-menu', action: 'display.showMenu($event.currentTarget, "views")', actionMods: '.prevent.stop', icon: Icons.chevronDown, attrs: [ 'title="Switch View"' ] }, { tplName: 'theme-menu', action: 'display.showMenu($event.currentTarget, "themes")', actionMods: '.prevent.stop', icon: Icons.layoutTextWindow, attrs: [ 'title="Switch Theme"' ] }, { tplName: 'untz', action: 'display.untz', icon: Icons.speaker, attrs: [ 'title="UNTZ"' ] } ]; /***/ }), /***/ "./src/components/user-template/index.js": /*!***********************************************!*\ !*** ./src/components/user-template/index.js ***! \***********************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const buttons = __webpack_require__(/*! ./buttons */ "./src/components/user-template/buttons.js"); // Regex for replacements const playingRE = /p: ?{([^}]*)}/g; const hoverRE = /h: ?{([^}]*)}/g; // Create a regex to find buttons/links, ignore matches if the button/link name is itself a regex. const tplNames = buttons.map(conf => `${conf.tplName.source && conf.tplName.source.replace(/\(/g, '(?:') || conf.tplName}`); const buttonRE = new RegExp(`(${tplNames.join('|')})-(?:button|link)(?:\\:"([^"]+?)")?`, 'g'); const soundTitleRE = /sound-title/g; const soundTitleMarqueeRE = /sound-title-marquee/g; const soundIndexRE = /sound-index/g; const soundCountRE = /sound-count/g; const soundPropRE = /sound-(src|id|name|post|imageOrThumb|image|thumb|filename|imageMD5)(-esc)?/g; const soundFilterCountRE = /filtered-count/g; const configRE = /\$config\[([^\]]+)\]/g; // Hold information on which config values components templates depend on. const componentDeps = [ ]; module.exports = { buttons, initialize() { 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(data) { const outerClass = data.outerClass || ''; const name = data.sound && data.sound.title || data.defaultName; let _data = { ...data }; const _confFuncOrText = v => (typeof v === 'function' ? v(_data) : v); // Apply common template replacements, unless they are opted out. let html = data.template.replace(configRE, (...args) => _.get(Player.config, args[1])); !data.ignoreDisplayBlocks && (html = html .replace(playingRE, Player.playing && Player.playing === data.sound ? '$1' : '') .replace(hoverRE, `$1`)); !data.ignoreButtons && (html = html.replace(buttonRE, function (full, type, text) { let buttonConf = Player.userTemplate._findButtonConf(type); _data.tplNameMatch = buttonConf.tplNameMatch; 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 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 }; } const attrs = [ ...(_confFuncOrText(buttonConf.attrs) || []) ]; attrs.some(attr => attr.startsWith('href')) || attrs.push('href="javascript:;"'); (buttonConf.class || outerClass) && attrs.push(`class="${buttonConf.class || ''} ${outerClass || ''}"`); buttonConf.action && attrs.push(`@click${buttonConf.actionMods || ''}='${_confFuncOrText(buttonConf.action)}'`); // Replace spaces with non breaking spaces in user text to prevent collapsing. return `${text && text.replace(/ /g, ' ') || _confFuncOrText(buttonConf.icon) || _confFuncOrText(buttonConf.text)}`; })); !data.ignoreSoundName && (html = html .replace(soundTitleMarqueeRE, name ? `
    ${name}
    ` : '') .replace(soundTitleRE, name ? `
    ${name}
    ` : '')); !data.ignoreSoundProperties && (html = html .replace(soundPropRE, (...args) => data.sound ? (args[2] ? _.escAttr(data.sound[args[1]], true) : data.sound[args[1]]) : '') .replace(soundIndexRE, data.sound ? Player.sounds.indexOf(data.sound) + 1 : 0) .replace(soundCountRE, Player.sounds.length) .replace(soundFilterCountRE, Player.filteredSounds.length)); !data.ignoreVersion && (html = html.replace(/%v/g, "3.5.0")); // 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(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(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 either showing properties of the playing sound or dependent on something playing. // order should render templates showing a sounds index. const hasCount = soundCountRE.test(template); const hasSoundProp = soundTitleRE.test(template) || soundPropRE.test(template); const hasIndex = soundIndexRE.test(template); const hasPlaying = playingRE.test(template); const hasFilterCount = soundFilterCountRE.test(template); hasCount && events.push('add', 'remove'); // The row template handles this itself to avoid a full playlist render. property !== 'rowTemplate' && (hasSoundProp || hasIndex || hasPlaying) && events.push('playsound', 'stop'); hasIndex && events.push('order'); hasFilterCount && events.push('filters-applied'); // 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 buttonConf = Player.userTemplate._findButtonConf(match[1]); if (buttonConf.property) { config.push(buttonConf.property); } } } // Find config references. while ((match = configRE.exec(template)) !== null) { config.push(match[1]); } return { events, config }; }, /** * When a config value is changed check if any component dependencies are affected. */ _handleConfig(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(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(); } }); }, _findButtonConf: type => { let tplNameMatch; let buttonConf = buttons.find(conf => { if (conf.tplName === type) { return tplNameMatch = [ type ]; } return tplNameMatch = conf.tplName.test && type.match(conf.tplName); }); return buttonConf && { ...buttonConf, tplNameMatch }; } }; /***/ }), /***/ "./src/config/display.js": /*!*******************************!*\ !*** ./src/config/display.js ***! \*******************************/ /***/ ((module) => { 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', allowInTheme: true }, { property: 'showUpdatedNotification', default: true, title: 'Show Update Notifications', description: 'Show notifications when the player is successfully updated.', displayGroup: 'Display' }, { property: 'hoverImages', title: 'Hover Images', default: false, allowInTheme: true }, { title: 'Controls', displayGroup: 'Display', allowInTheme: true, 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
    ' + '
    volume-button
    ' + '
    volume-bar
    ' + 'and
    fullscreen
    .', default: [ 'fullscreen', 'duration', 'volume-bar', 'seek-bar', 'time', 'previous' ], displayMethod: 'textarea', inlineTextarea: true, format: v => v.join('\n'), parse: v => v.split(/\s+/) } ] }, { title: 'Minimised Display', description: 'Optional displays for when the player is minimised.', displayGroup: 'Display', allowInTheme: true, 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', updateCSSVars: true }, { property: 'chanXControls', title: '4chan X Header Controls', description: 'Show playback controls in the 4chan X header. The display can be customised in Settings>Theme.', displayMethod: isChanX || null, default: 'closed', options: { always: 'Always', closed: 'Only with the player closed', never: 'Never' } } ] }, { title: 'Thread', displayGroup: 'Display', allowInTheme: true, 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.', settings: [ { title: 'Display', default: 'table', options: { table: 'Table', board: 'Board' } } ] }, { title: 'Colors', displayGroup: 'Display', property: 'colors', updateCSSVars: true, allowInTheme: true, class: `${ns}-colorpicker-input`, attrs: '@focusout.stop.prevent="colorpicker._updatePreview" @click="colorpicker.create"', displayMethod: ({ value, attrs }) => `
    `, actions: [ { title: 'Match Theme', handler: 'theme.forceBoardTheme', mods: '.prevent' } ], // These colors will be overriden with the theme defaults at initialization. They're set to yotsuba b here. 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("colors.controls_background")', mods: '.prevent' } ], }, { property: 'colors.controls_inactive', default: '#FFFFFF', title: 'Control Items', description: 'The playback controls and played bar.', actions: [ { title: 'Reset', handler: 'settings.reset("colors.controls_inactive")', mods: '.prevent' } ], }, { property: 'colors.controls_active', default: '#00b6f0', title: 'Focused Control Items', description: 'The control items when hovered.', actions: [ { title: 'Reset', handler: 'settings.reset("colors.controls_active")', mods: '.prevent' } ], }, { 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("colors.controls_empty_bar")', mods: '.prevent' } ], }, { 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("colors.controls_loaded_bar")', mods: '.prevent' } ], }, // Not configurable but here for access in templates. { property: 'colors.page_background', default: 'rgb(238, 242, 255)', displayMethod: null, allowInTheme: false } ] } ]; /***/ }), /***/ "./src/config/filter.js": /*!******************************!*\ !*** ./src/config/filter.js ***! \******************************/ /***/ ((module) => { 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 sounds are allowed to be loaded from.', default: [ '4cdn.org', 'catbox.moe', 'dmca.gripe', 'lewd.se', 'pomf.cat', 'zz.ht', 'zz.fo' ], actions: [ { title: 'Reset', handler: 'settings.reset("allow")', mods: '.prevent' } ], displayGroup: 'Filter', displayMethod: 'textarea', attrs: 'rows=10', format: v => v.join('\n'), parse: v => v.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("filters")', mods: '.prevent' } ], displayGroup: 'Filter', displayMethod: 'textarea', attrs: 'rows=10', format: v => v.join('\n'), parse: v => v.split('\n') } ]; /***/ }), /***/ "./src/config/hosts.js": /*!*****************************!*\ !*** ./src/config/hosts.js ***! \*****************************/ /***/ ((module) => { module.exports = [ { property: 'defaultUploadHost', default: 'catbox', parse: 'settings.hosts.setDefault' }, { property: 'uploadHosts', title: 'Hosts', actions: [ { title: 'Add', handler: 'settings.hosts.add', mods: '.prevent' }, { title: 'Restore Defaults', handler: 'settings.hosts.restoreDefaults', mods: '.prevent' } ], displayGroup: 'Hosts', displayMethod: 'settings.hosts.template', parse: 'settings.hosts.parse', looseCompare: true, wideDesc: true, description: 'Each host needs a unique name and URL that points to an upload endpoint. ' + '

    The form data is a JSON representation of the data sent with the upload, with the file being indicated as "$file". ' + 'The form data and headers allow for any other information to be sent, such as a user token.' + '

    A response path or match can optionally be provided to get a link to the uploaded file from the response. ' + 'Use "Response Path" for JSON responses to set where a link or filename can be found in the response. ' + 'For all other responses "Response Match" takes a regular expression (without slashes) that is applied to the result, with the first capture group being the link or filename. ' + 'File URL format can be set if you only have part of the link, such as the filename. The response, or response path/match result, will be inserted in place of %s.', 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', soundUrl: 'a.pomf.cat/%s', 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 ***! \*****************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { module.exports = [ // Order the groups appear in. ...__webpack_require__(/*! ./display */ "./src/config/display.js"), ...__webpack_require__(/*! ./playback */ "./src/config/playback.js"), ...__webpack_require__(/*! ./filter */ "./src/config/filter.js"), ...__webpack_require__(/*! ./keybinds */ "./src/config/keybinds.js"), ...__webpack_require__(/*! ./theme */ "./src/config/theme.js"), ...__webpack_require__(/*! ./hosts */ "./src/config/hosts.js"), { property: 'viewStyle', default: 'playlist' }, { property: 'showPlaylistSearch', default: true }, { property: 'imageHeight', default: 125 }, // These are for availability in templates { property: 'offsetTop', default: '0' }, { property: 'offsetBottom', default: '0' } ]; /***/ }), /***/ "./src/config/keybinds.js": /*!********************************!*\ !*** ./src/config/keybinds.js ***! \********************************/ /***/ ((module) => { const hasMediaSession = 'mediaSession' in navigator; const keybindOpts = { displayGroup: 'Keybinds', format: 'hotkeys.stringifyKey', parse: 'hotkeys.parseKey', attrs: '@keydown="settings.handleKeyChange"', property: 'hotkey_bindings', allowInTheme: true }; module.exports = [ { title: 'Keybinds', displayGroup: 'Keybinds', settings: [ { property: 'hotkeys', default: 'open', title: 'Enabled', options: { always: 'Always', open: 'Only with the player open', never: 'Never' } }, { 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: 'Playback', themeFieldTitle: 'Playback Keybinds', ...keybindOpts, settings: [ { 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: 'actions.volumeUp', default: { shiftKey: true, key: 'arrowup' } }, { property: 'hotkey_bindings.volumeDown', title: 'Volume Down', keyHandler: 'actions.volumeDown', default: { shiftKey: true, key: 'arrowdown' } }, { property: 'hotkey_bindings.shuffle', title: 'Shuffle', keyHandler: 'playlist.toggleShuffle', default: { key: '' } }, { property: 'hotkey_bindings.repeat', title: 'Toggle Repeat', keyHandler: 'playlist.toggleRepeat', default: { key: '' } } ] }, { title: 'Display', themeFieldTitle: 'Display Keybinds', ...keybindOpts, settings: [ { 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: '' }, allowFocusedInput: true }, { 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: '' }, allowFocusedInput: true }, { property: 'hotkey_bindings.scrollToPlaying', title: 'Jump To Playing', keyHandler: () => Player.playlist.scrollToPlaying(), default: { key: '' } }, { property: 'hotkey_bindings.toggleHoverImages', title: 'Toggle Hover Images', keyHandler: 'playlist.toggleHoverImages', default: { key: '' } }, { property: 'hotkey_bindings.toggleAutoScroll', title: 'Toggle Thread Scroll', keyHandler: () => Player.set('autoScrollThread', !Player.config.autoScrollThread), default: { key: '' } } ] }, { title: 'Theme', themeFieldTitle: 'Theme Keybinds', ...keybindOpts, settings: [ { property: 'hotkey_bindings.nextTheme', title: 'Next Theme', keyHandler: 'theme.next', default: { key: '' } }, { property: 'hotkey_bindings.previousTheme', title: 'Previous Theme', keyHandler: 'theme.previous', default: { key: '' } }, { property: 'hotkey_bindings.switchTheme', title: 'Select Theme', keyHandler: 'theme.handleSwitch', default: [ ], displayMethod: 'theme.themeKeybindsTemplate', parse: 'theme.parseSwitch', format: null } ] } ]; /***/ }), /***/ "./src/config/playback.js": /*!********************************!*\ !*** ./src/config/playback.js ***! \********************************/ /***/ ((module) => { module.exports = [ { property: 'shuffle', title: 'Shuffle', displayGroup: 'Playback', default: false }, { property: 'repeat', title: 'Repeat', displayGroup: 'Playback', default: 'all', options: { all: 'All', one: 'One', none: 'None' } }, { property: 'preventSleep', title: 'Prevent Sleep', displayGroup: 'Playback', description: 'Prevent sleeping while audio is playing. This only works when the browser and tab are in the foreground.', default: true }, { property: 'autoplayNext', title: 'Autoplay Next', displayGroup: 'Playback', description: 'Automatically play the next sound when the current one finishes.', default: true }, { property: 'restartSeconds', title: 'Restart After', displayGroup: 'Playback', description: 'How long into a track until selecting previous restarts the track instead. Set to 0 to disable.', default: 3, parse: v => +v >= 0 && +v < Infinity ? +v : 0 }, { title: 'Inline Player', displayGroup: 'Playback', settings: [ { property: 'playExpandedImages', title: 'Expanded Image', description: 'Play audio when sound images are expanded.', default: true, dependentRender: [ 'expandedControls' ] }, { property: 'expandedControls', title: 'Expanded Controls', description: 'Show playback controls for expanded images.', default: true, attrs: () => !Player.config.playExpandedImages ? 'disabled' : '' }, { property: 'expandedLoopMaster', title: 'Master Source', default: 'sound', description: 'Which media source to play in full for audio and video of different durations.', options: { sound: 'Audio', video: 'Video', // longest: 'Longest' } }, { property: 'expandedAllowFiltered', title: 'Allow Filtered', default: true, description: 'Allow sounds that have been filtered to be played inline. ' + 'Sounds from unknown hosts will not be played regardless of this setting.', }, { property: 'expandedRepeat', title: 'Repeat', default: 'all', description: 'How to repeat expanded images with multiple sounds.', options: { all: 'All', one: 'One', none: 'None' } }, { property: 'playHoveredImages', title: 'Hover Image', description: 'Play audio when sound hover images are shown. This applies to hover images displayed by the native extention or 4chan X.', default: true } ] } ]; /***/ }), /***/ "./src/config/theme.js": /*!*****************************!*\ !*** ./src/config/theme.js ***! \*****************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { module.exports = [ { property: 'savedThemes', title: 'Saved Themes', actions: [ { title: 'Restore Defaults', handler: 'theme.restoreDefaults', mods: '.prevent' }, { title: 'Save Current', handler: 'theme.showSaveOptions', mods: '.prevent.stop' } ], displayGroup: 'Theme', displayMethod: 'theme.savedThemesTemplate', mix: true, default: __webpack_require__(/*! ../components/theme/themes.js */ "./src/components/theme/themes.js") }, { property: 'savedThemesOrder', default: [ ], }, { property: 'selectedTheme', default: 'Default' }, { property: 'headerTemplate', title: 'Header', actions: [ { title: 'Reset', handler: 'settings.reset("headerTemplate")', mods: '.prevent' } ], default: 'repeat-button shuffle-button hover-images-button playlist-button\nsound-title-marquee\nview-menu-button add-button theme-menu-button close-button', displayGroup: 'Theme', displayMethod: 'textarea', themeField: true }, { property: 'rowTemplate', title: 'Row', actions: [ { title: 'Reset', handler: 'settings.reset("rowTemplate")', mods: '.prevent' } ], default: 'sound-title h:{menu-button}', displayGroup: 'Theme', displayMethod: 'textarea', themeField: true }, { property: 'footerTemplate', title: 'Footer', actions: [ { title: 'Reset', handler: 'settings.reset("footerTemplate")', mods: '.prevent' } ], default: 'playing-button:"sound-index / " sound-count sounds\n' + '
    \n' + 'p:{\n' + ' post-link\n' + ' Open [ image-link sound-link ]\n' + ' Download [ dl-image-button dl-sound-button ]\n' + '}\n' + `
    \n`, displayGroup: 'Theme', displayMethod: 'textarea', attrs: 'rows="10"', themeField: true }, { property: 'chanXTemplate', title: '4chan X Header', default: 'p:{\n\tpost-link:"sound-title"\n\tprev-button\n\tplay-button\n\tnext-button\n\tsound-current-time / sound-duration\n}', actions: [ { title: 'Reset', handler: 'settings.reset("chanXTemplate")', mods: '.prevent' } ], displayGroup: 'Theme', displayMethod: 'textarea', attrs: 'rows="10"', themeField: true }, { property: 'customCSS', title: 'Custom CSS', default: '', displayGroup: 'Theme', displayMethod: 'textarea', attrs: 'rows="10"', themeField: true } ]; /***/ }), /***/ "./src/globals.js": /*!************************!*\ !*** ./src/globals.js ***! \************************/ /***/ (() => { /** * Global variables and helpers. */ window.ns = 'fcsp'; 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]; window.Thread = (location.href.match(/\/thread\/(\d+)/) || [])[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 ***! \**********************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { module.exports = { fcSounds: '4sp', 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, arrowDown: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/arrow-down.svg */ "./node_modules/bootstrap-icons/icons/arrow-down.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, arrowUp: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/arrow-up.svg */ "./node_modules/bootstrap-icons/icons/arrow-up.svg").default, boxArrowInLeft: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/box-arrow-in-left.svg */ "./node_modules/bootstrap-icons/icons/box-arrow-in-left.svg").default, boxArrowRight: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/box-arrow-right.svg */ "./node_modules/bootstrap-icons/icons/box-arrow-right.svg").default, chatRightQuote: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/chat-right-quote.svg */ "./node_modules/bootstrap-icons/icons/chat-right-quote.svg").default, checkSquare: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/check-square.svg */ "./node_modules/bootstrap-icons/icons/check-square.svg").default, chevronDown: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/chevron-down.svg */ "./node_modules/bootstrap-icons/icons/chevron-down.svg").default, chevronUp: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/chevron-up.svg */ "./node_modules/bootstrap-icons/icons/chevron-up.svg").default, close: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/x.svg */ "./node_modules/bootstrap-icons/icons/x.svg").default.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, github: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/github.svg */ "./node_modules/bootstrap-icons/icons/github.svg").default, image: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/image.svg */ "./node_modules/bootstrap-icons/icons/image.svg").default, infoCircle: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/info-circle.svg */ "./node_modules/bootstrap-icons/icons/info-circle.svg").default, layoutTextWindow: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/layout-text-window.svg */ "./node_modules/bootstrap-icons/icons/layout-text-window.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, speaker: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/speaker.svg */ "./node_modules/bootstrap-icons/icons/speaker.svg").default, square: __webpack_require__(/*! ../node_modules/bootstrap-icons/icons/square.svg */ "./node_modules/bootstrap-icons/icons/square.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/components/controls/templates/controls.tpl": /*!********************************************************!*\ !*** ./src/components/controls/templates/controls.tpl ***! \********************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => `
    ${data.inline && !data.multiple ? '' : `
    ${Icons.skipStart} ${Icons.skipStartFill}
    `}
    ${Icons.play} ${Icons.pause} ${Icons.playFill} ${Icons.pauseFill}
    ${data.inline && !data.multiple ? '' : `
    ${Icons.skipEnd} ${Icons.skipEndFill}
    `}
    0:00 / 0:00
    ${Icons.volumeMute} ${Icons.volumeMuteFill} ${Icons.volumeUp} ${Icons.volumeUpFill}
    ${data.inline ? '' : `
    ${Icons.fullscreen} ${Icons.fullscreenExit}
    `}
    ` /***/ }), /***/ "./src/components/display/templates/body.tpl": /*!***************************************************!*\ !*** ./src/components/display/templates/body.tpl ***! \***************************************************/ /***/ ((module) => { module.exports = (data = {}) => `` /***/ }), /***/ "./src/components/display/templates/themes_menu.tpl": /*!**********************************************************!*\ !*** ./src/components/display/templates/themes_menu.tpl ***! \**********************************************************/ /***/ ((module) => { module.exports = (data = {}) => `` /***/ }), /***/ "./src/components/display/templates/views_menu.tpl": /*!*********************************************************!*\ !*** ./src/components/display/templates/views_menu.tpl ***! \*********************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => `` /***/ }), /***/ "./src/components/playlist/templates/item_menu.tpl": /*!*********************************************************!*\ !*** ./src/components/playlist/templates/item_menu.tpl ***! \*********************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); module.exports = (data = {}) => `` /***/ }), /***/ "./src/components/playlist/templates/list.tpl": /*!****************************************************!*\ !*** ./src/components/playlist/templates/list.tpl ***! \****************************************************/ /***/ ((module) => { module.exports = (data = {}) => (data.sounds || Player.sounds).map(sound => !Player.playlist.matchesSearch(sound) ? '' : `
    ${Player.userTemplate.build({ template: Player.config.rowTemplate, location: 'item-' + sound.id, sound, outerClass: `${ns}-col-auto` })}
    ` ).join('') /***/ }), /***/ "./src/components/playlist/templates/player.tpl": /*!******************************************************!*\ !*** ./src/components/playlist/templates/player.tpl ***! \******************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => `
    ${Player.controls.template({ audio: Player.audio, audioId: Player.audio.dataset.id, actions: Player.controls.actions })}
    ${Player.playlist.listTemplate()}
    ` /***/ }), /***/ "./src/components/playlist/templates/tags_dialog.tpl": /*!***********************************************************!*\ !*** ./src/components/playlist/templates/tags_dialog.tpl ***! \***********************************************************/ /***/ ((module) => { module.exports = (data = {}) => { if (!data.tags) { return '
    Loading
    '; } const tagsArr = Object.entries(data.tags); if (!tagsArr.length) { return '
    No data
    '; } return tagsArr.map(([ name, value ]) => `
    ${name[0].toUpperCase() + name.slice(1)}: ${value}
    `).join(''); } /***/ }), /***/ "./src/components/settings/templates/colorpicker.tpl": /*!***********************************************************!*\ !*** ./src/components/settings/templates/colorpicker.tpl ***! \***********************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => `
    ${Icons.close}
    ${['R','G','B','A'].map((n, i) => ` `).join('')}
    ${n}:

    ` /***/ }), /***/ "./src/components/settings/templates/host_input.tpl": /*!**********************************************************!*\ !*** ./src/components/settings/templates/host_input.tpl ***! \**********************************************************/ /***/ ((module) => { module.exports = (data = {}) => Object.entries(Player.config.uploadHosts).map(([ name, host ]) => `
    ${Object.entries(Player.settings.hosts.fields).map(([ field, title ]) => `
    `).join('')}
    `).join('') /***/ }), /***/ "./src/components/settings/templates/inputs/checkbox.tpl": /*!***************************************************************!*\ !*** ./src/components/settings/templates/inputs/checkbox.tpl ***! \***************************************************************/ /***/ ((module) => { module.exports = (data = {}) => `
    ` /***/ }), /***/ "./src/components/settings/templates/inputs/input.tpl": /*!************************************************************!*\ !*** ./src/components/settings/templates/inputs/input.tpl ***! \************************************************************/ /***/ ((module) => { module.exports = (data = {}) => `
    ` /***/ }), /***/ "./src/components/settings/templates/inputs/select.tpl": /*!*************************************************************!*\ !*** ./src/components/settings/templates/inputs/select.tpl ***! \*************************************************************/ /***/ ((module) => { module.exports = (data = {}) => `
    ` /***/ }), /***/ "./src/components/settings/templates/inputs/textarea.tpl": /*!***************************************************************!*\ !*** ./src/components/settings/templates/inputs/textarea.tpl ***! \***************************************************************/ /***/ ((module) => { module.exports = (data = {}) => `
    ` /***/ }), /***/ "./src/components/settings/templates/setting.tpl": /*!*******************************************************!*\ !*** ./src/components/settings/templates/setting.tpl ***! \*******************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => data.displayMethod === null ? '' : `
    ${data.title} ${data.description ? `${Icons.infoCircle}` : ''} ${!data.actions || !data.actions.length ? '' : `
    ${(data.actions || []).map(action => ` ${action.title} `).join(' ')}
    `}
    ${data.dismissTextId ? `
    ${Player.display.ifNotDismissed( data.dismissTextId, data.dismissRestoreText, `
    ${data.text} Dismiss
    ` )}
    ` : data.text ? data.text : ''} ${(() => { // Recusively call for sub settings. if (data.settings) { return data.settings.map(subSetting => Player.settings.settingTemplate({ ...data, actions: null, settings: null, description: null, ...subSetting, isSubSetting: true })).join('') } value = _.get(Player.config, data.property, data.default); attrs = (typeof data.attrs === 'function' ? data.attrs() : data.attrs || '') + (data.class ? ` class="${data.class}"` : '') + ` data-property="${data.property}"`; if (data.format) { value = Player.getHandler(data.format)(value); } let type = typeof value; if (type === 'object') { value = JSON.stringify(value, null, 4) } inputTemplate = typeof data.displayMethod === 'function' && data.displayMethod || _.get(Player, data.displayMethod) || data.displayMethod && Player.settings.inputTemplates[data.displayMethod] || type === 'boolean' && Player.settings.inputTemplates.checkbox || type === 'object' && Player.settings.inputTemplates.textarea || data.options && Player.settings.inputTemplates.select || Player.settings.inputTemplates.input; return inputTemplate({ value, attrs, setting: data }); })()}
    ` /***/ }), /***/ "./src/components/settings/templates/settings.tpl": /*!********************************************************!*\ !*** ./src/components/settings/templates/settings.tpl ***! \********************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => `
    ${Object.keys(Player.settings.groups).map(name => ` ${name} ` ).join(' | ')}
    ${Object.entries(Player.settings.groups).map(([ name, settings ]) => `
    ${settings.map(Player.settings.settingTemplate).join('')}
    `).join('')}` /***/ }), /***/ "./src/components/theme/templates/save_theme_menu.tpl": /*!************************************************************!*\ !*** ./src/components/theme/templates/save_theme_menu.tpl ***! \************************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => `
    Included Settings ${Icons.chevronDown} ${Icons.chevronUp} ${(function saveFieldOptions(settings, parent) { return settings.map(s => { const id = s.property && `theme_field-${s.property.replace(/\./g, '_')}`; return s.settings ? saveFieldOptions(s.settings, s) : { ...(parent || {}), ...s }.allowInTheme || s.themeField ? `
    ` : ''; }).join(''); })(data.settingsConfig)}
    ` /***/ }), /***/ "./src/components/theme/templates/saved_themes.tpl": /*!*********************************************************!*\ !*** ./src/components/theme/templates/saved_themes.tpl ***! \*********************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => `` /***/ }), /***/ "./src/components/theme/templates/theme_keybinds.tpl": /*!***********************************************************!*\ !*** ./src/components/theme/templates/theme_keybinds.tpl ***! \***********************************************************/ /***/ ((module) => { module.exports = (data = {}) => [ 'Default' ].concat(Player.config.savedThemesOrder).map(name => `
    - ${name}
    `).join('') /***/ }), /***/ "./src/components/threads/templates/boards.tpl": /*!*****************************************************!*\ !*** ./src/components/threads/templates/boards.tpl ***! \*****************************************************/ /***/ ((module) => { module.exports = (data = {}) => (Player.threads.boardList || []).map(board => { let checked = Player.threads.selectedBoards.includes(board.board); return !checked && !Player.threads.showAllBoards ? '' : ` ` }).join('') /***/ }), /***/ "./src/components/threads/templates/list.tpl": /*!***************************************************!*\ !*** ./src/components/threads/templates/list.tpl ***! \***************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); module.exports = (data = {}) => Object.keys(Player.threads.displayThreads).reduce((rows, board) => { return rows.concat(Player.threads.displayThreads[board].map(thread => ` >>>/${thread.board}/${thread.no} ${thread.sub || ''} ${thread.replies} / ${thread.images} ${_.timeAgo(thread.time)} ${_.timeAgo(thread.last_modified)} `)) }, []).join('') /***/ }), /***/ "./src/components/threads/templates/threads.tpl": /*!******************************************************!*\ !*** ./src/components/threads/templates/threads.tpl ***! \******************************************************/ /***/ ((module) => { module.exports = (data = {}) => `
    Active Threads ${!Player.threads.loading ? `- Update` : ''}
    Loading
    Filter
    ${Player.threads.boardsTemplate(data)}
    ${!Player.threads.hasParser ? '' : `
    ${Player.config.threadsViewStyle !== 'table' ? `Table` : `Table`} | ${Player.config.threadsViewStyle !== 'board' ? `Board` : `Board`}
    ` } ${ !Player.threads.hasParser || Player.config.threadsViewStyle === 'table' ? `
    Thread Subject Replies/Images Started Updated
    ` : `
    ` }
    ` /***/ }), /***/ "./src/components/tools/templates/create-complete.tpl": /*!************************************************************!*\ !*** ./src/components/tools/templates/create-complete.tpl ***! \************************************************************/ /***/ ((module) => { module.exports = (data = {}) => `
    Complete!
    ${is4chan ? 'Post - ' : ''} Add - Download
    ` /***/ }), /***/ "./src/components/tools/templates/create.tpl": /*!***************************************************!*\ !*** ./src/components/tools/templates/create.tpl ***! \***************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => `
    Create Sound Image
    ${Player.display.ifNotDismissed('createSoundDetails', 'Show Help', `
    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.

    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.
    Dismiss
    `)}
    Host - Config
    ${Player.tools.createHostsTemplate()}
    Data
    Select/Drop Image
    ${Player.tools._createdImage ? Player.tools.createCompleteTemplate() : ''}
    ` /***/ }), /***/ "./src/components/tools/templates/download-progress.tpl": /*!**************************************************************!*\ !*** ./src/components/tools/templates/download-progress.tpl ***! \**************************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => `
    ${Icons.close}
    ${Icons.image}
    ${Icons.soundwave}
    ` /***/ }), /***/ "./src/components/tools/templates/download.tpl": /*!*****************************************************!*\ !*** ./src/components/tools/templates/download.tpl ***! \*****************************************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); module.exports = (data = {}) => `
    Download All
    Download Concurrency
    Compression Level ${Icons.infoCircle}
    Max Sounds ${Icons.infoCircle}
    Clear${Icons.infoCircle}
    ` /***/ }), /***/ "./src/components/tools/templates/hosts-select.tpl": /*!*********************************************************!*\ !*** ./src/components/tools/templates/hosts-select.tpl ***! \*********************************************************/ /***/ ((module) => { module.exports = (data = {}) => `` /***/ }), /***/ "./src/components/tools/templates/tools.tpl": /*!**************************************************!*\ !*** ./src/components/tools/templates/tools.tpl ***! \**************************************************/ /***/ ((module) => { module.exports = (data = {}) => `${Player.tools.createTemplate()} ${Player.tools.downloadTemplate()}
    Encode / Decode URL
    ` /***/ }), /***/ "./src/scss/4chan-x-polyfill.scss": /*!****************************************!*\ !*** ./src/scss/4chan-x-polyfill.scss ***! \****************************************/ /***/ ((module) => { module.exports = (data = {}) => `.dialog { background: var(--fcsp-background); background: var(--fcsp-background); border-color: var(--fcsp-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/root.scss": /*!****************************!*\ !*** ./src/scss/root.scss ***! \****************************/ /***/ ((module) => { module.exports = (data = {}) => `:root { --fcsp-text: ${Player.config.colors.text}; --fcsp-background: ${Player.config.colors.background}; --fcsp-border: ${Player.config.colors.border}; --fcsp-odd-row: ${Player.config.colors.odd_row}; --fcsp-even-row: ${Player.config.colors.even_row}; --fcsp-playing: ${Player.config.colors.playing}; --fcsp-dragging: ${Player.config.colors.dragging}; --fcsp-controls-background: ${Player.config.colors.controls_background}; --fcsp-controls-active: ${Player.config.colors.controls_active}; --fcsp-controls-inactive: ${Player.config.colors.controls_inactive}; --fcsp-controls-empty-bar: ${Player.config.colors.controls_empty_bar}; --fcsp-controls-loaded-bar: ${Player.config.colors.controls_loaded_bar}; --fcsp-muted: #909090; --fcsp-max-pip-size: ${Player.config.maxPIPWidth}; }` /***/ }), /***/ "./src/scss/style.scss": /*!*****************************!*\ !*** ./src/scss/style.scss ***! \*****************************/ /***/ ((module) => { 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: var(--fcsp-muted); } .${ns}-controls { align-items: center; padding: 0.5rem 0; position: relative; background: var(--fcsp-controls-background); justify-content: space-between; } .${ns}-controls > div { margin: 0 0.5rem; } .${ns}-controls .${ns}-current-time { color: var(--fcsp-controls-inactive); } .${ns}-controls .${ns}-media-control { width: 1.5rem; height: 1.5rem; font-size: 1rem; color: var(--fcsp-controls-inactive); } .${ns}-controls .${ns}-media-control.disabled { cursor: inherit; filter: brightness(0.5) grayscale(1); } .${ns}-controls .${ns}-media-control:hover:not(.disabled) { color: var(--fcsp-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:not(.disabled) svg { display: none; } .${ns}-media-control.${ns}-hover-fill:hover:not(.disabled) svg[class$=-fill], .${ns}-media-control.${ns}-hover-fill:hover:not(.disabled) 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:hover .${ns}-current-bar:after { background: var(--fcsp-controls-active); } .${ns}-full-bar { height: 0.3rem; width: 100%; background: var(--fcsp-controls-empty-bar); border-radius: 1rem; position: relative; } .${ns}-full-bar > div { position: absolute; top: 0; bottom: 0; border-radius: 1rem; } .${ns}-full-bar .${ns}-loaded-bar { background: var(--fcsp-controls-loaded-bar); } .${ns}-full-bar .${ns}-current-bar { display: flex; justify-content: flex-end; align-items: center; } .${ns}-full-bar .${ns}-current-bar:after { content: ""; background: var(--fcsp-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}-seek-bar .${ns}-current-bar { background: var(--fcsp-controls-active); } .${ns}-volume-bar .${ns}-current-bar { background: var(--fcsp-controls-inactive); } .${ns}-chan-x-controls .${ns}-current-time, .${ns}-chan-x-controls .${ns}-duration { margin: 0 0.25rem; } .${ns}-header, .${ns}-footer { text-align: center; padding: 0.125rem; max-width: 100%; line-height: 1rem; } .${ns}-header .${ns}-expander, .${ns}-footer .${ns}-expander { opacity: 0; position: absolute; bottom: -0.25rem; right: -0.25rem; 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%, var(--fcsp-border) 55%, var(--fcsp-border) 100%); } .${ns}-header .${ns}-expander[data-direction=sw], .${ns}-footer .${ns}-expander[data-direction=sw] { left: -0.25rem; right: auto; transform: rotate(90deg); cursor: sw-resize; } .${ns}-header .${ns}-expander[data-direction=nw], .${ns}-footer .${ns}-expander[data-direction=nw] { left: -0.25rem; top: -0.25rem; right: auto; bottom: auto; transform: rotate(180deg); cursor: nw-resize; } .${ns}-header .${ns}-expander[data-direction=ne], .${ns}-footer .${ns}-expander[data-direction=ne] { right: -0.25rem; top: -0.25rem; bottom: auto; transform: rotate(270deg); cursor: ne-resize; } .${ns}-header { cursor: grab; border-bottom: solid 1px var(--fcsp-border); } .${ns}-footer { cursor: grab; border-top: solid 1px var(--fcsp-border); } .${ns}-title-marquee { transition: margin-left 1s linear; } .${ns}-menu svg.bi { margin: 0 -0.25rem 0 0.25rem; } .${ns}-header svg.bi, .${ns}-footer svg.bi { margin: 0 0.125rem; } .muted svg.bi { opacity: 0.45; } svg.bi { overflow: visible; height: 1em; width: 1em; } .fileText svg.bi { vertical-align: text-bottom; } .${ns}-image-link { text-align: center; display: flex; flex-grow: 1; justify-content: center; align-items: center; overflow: hidden; position: relative; } .${ns}-image-link.${ns}-show-video .${ns}-video { display: block; } .${ns}-image-link.${ns}-show-video .${ns}-image { display: none; } .${ns}-image-link.${ns}-pip { position: fixed; right: 10px; align-items: end; } .${ns}-image-link.${ns}-pip .${ns}-image, .${ns}-image-link.${ns}-pip .${ns}-video { width: auto; height: auto; max-height: var(--fcsp-max-pip-size); max-width: var(--fcsp-max-pip-size); object-fit: contain; } .${ns}-image-link.${ns}-pip .${ns}-image-transparent-bg, .${ns}-image-link.${ns}-pip .${ns}-background-image { display: none; } .${ns}-image-link .${ns}-video { display: none; } .${ns}-image-link .${ns}-image, .${ns}-image-link .${ns}-video { height: 100%; width: 100%; object-fit: contain; z-index: 1; } .${ns}-image-link .${ns}-background-image { position: absolute; top: -20px; bottom: -20px; left: -20px; right: -20px; object-fit: cover; object-position: center; height: calc(100% + 40px); width: calc(100% + 40px); filter: blur(10px) brightness(0.7) saturate(0.5) contrast(0.7); z-index: -1; } .${ns}-image-link .${ns}-background-image:not([src]) { display: none; } .${ns}-image-link .${ns}-image-transparent-bg { background-color: var(--fcsp-background); position: absolute; } .untz .${ns}-image-link { transition: all 0.1s linear; } #${ns}-container[data-view-style=fullscreen].playing-video .${ns}-background-image { display: none; } #${ns}-container[data-view-style=fullscreen].playing-video .${ns}-image-link, #${ns}-container[data-view-style=fullscreen] .${ns}-image-transparent-bg { background: black; } #${ns}-container.playing-video .${ns}-image-transparent-bg { display: none; } .image-color-overlay { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background: rgba(0, 255, 0, 0.5); transition: all 0.5s linear; display: none; } .untz .image-color-overlay { display: block; } .${ns}-has-controls { position: relative; } .${ns}-has-controls .${ns}-controls { cursor: auto; flex-wrap: nowrap; overflow: hidden; min-width: auto; position: absolute; left: 0; right: 0; bottom: 0; } .${ns}-has-controls:not(:hover) .${ns}-controls { display: none; } html:not(.fourchan-x) .${ns}-has-controls > video + .${ns}-controls { left: 20px; right: 20px; } #${ns}-container { position: fixed; display: flex; flex-direction: column; background: var(--fcsp-background); color: var(--fcsp-text); border: 1px solid var(--fcsp-border); box-sizing: border-box; min-width: 7rem; line-height: 1rem; } #${ns}-container .w-100 { width: 100%; min-width: 100%; } #${ns}-container input[type=text], #${ns}-container input[type=number], #${ns}-container select, #${ns}-container .${ns}-file-overlay { background: white; color: black; height: 1.5rem !important; padding: 2px 4px 3px; box-sizing: border-box; margin: 0; border-radius: 0; border: solid 1px var(--fcsp-border); padding: 0 0.25rem; } #${ns}-container .m-0 { margin: 0; } #${ns}-container .mx-0 { margin-right: 0; margin-left: 0; } #${ns}-container .my-0 { margin-top: 0; margin-bottom: 0; } #${ns}-container .mr-0 { margin-right: 0; } #${ns}-container .ml-0 { margin-left: 0; } #${ns}-container .mt-0 { margin-top: 0; } #${ns}-container .mb-0 { margin-bottom: 0; } #${ns}-container .m-1 { margin: 0.125rem; } #${ns}-container .mx-1 { margin-right: 0.125rem; margin-left: 0.125rem; } #${ns}-container .my-1 { margin-top: 0.125rem; margin-bottom: 0.125rem; } #${ns}-container .mr-1 { margin-right: 0.125rem; } #${ns}-container .ml-1 { margin-left: 0.125rem; } #${ns}-container .mt-1 { margin-top: 0.125rem; } #${ns}-container .mb-1 { margin-bottom: 0.125rem; } #${ns}-container .m-2 { margin: 0.25rem; } #${ns}-container .mx-2 { margin-right: 0.25rem; margin-left: 0.25rem; } #${ns}-container .my-2 { margin-top: 0.25rem; margin-bottom: 0.25rem; } #${ns}-container .mr-2 { margin-right: 0.25rem; } #${ns}-container .ml-2 { margin-left: 0.25rem; } #${ns}-container .mt-2 { margin-top: 0.25rem; } #${ns}-container .mb-2 { margin-bottom: 0.25rem; } #${ns}-container .m-3 { margin: 0.375rem; } #${ns}-container .mx-3 { margin-right: 0.375rem; margin-left: 0.375rem; } #${ns}-container .my-3 { margin-top: 0.375rem; margin-bottom: 0.375rem; } #${ns}-container .mr-3 { margin-right: 0.375rem; } #${ns}-container .ml-3 { margin-left: 0.375rem; } #${ns}-container .mt-3 { margin-top: 0.375rem; } #${ns}-container .mb-3 { margin-bottom: 0.375rem; } #${ns}-container .m-4 { margin: 0.5rem; } #${ns}-container .mx-4 { margin-right: 0.5rem; margin-left: 0.5rem; } #${ns}-container .my-4 { margin-top: 0.5rem; margin-bottom: 0.5rem; } #${ns}-container .mr-4 { margin-right: 0.5rem; } #${ns}-container .ml-4 { margin-left: 0.5rem; } #${ns}-container .mt-4 { margin-top: 0.5rem; } #${ns}-container .mb-4 { margin-bottom: 0.5rem; } #${ns}-container .m-5 { margin: 1rem; } #${ns}-container .mx-5 { margin-right: 1rem; margin-left: 1rem; } #${ns}-container .my-5 { margin-top: 1rem; margin-bottom: 1rem; } #${ns}-container .mr-5 { margin-right: 1rem; } #${ns}-container .ml-5 { margin-left: 1rem; } #${ns}-container .mt-5 { margin-top: 1rem; } #${ns}-container .mb-5 { margin-bottom: 1rem; } #${ns}-container .pointer { cursor: pointer; } .${ns}-panel { padding: 0 0.25rem; height: 100%; width: calc(100% - .5rem); overflow: auto; } .${ns}-heading { font-weight: 600; margin: 0.5rem 0; } .${ns}-heading.lined { border-bottom: solid 1px var(--fcsp-border); } .${ns}-heading.lined .${ns}-heading-action { text-decoration: none; } .${ns}-heading-action { font-weight: normal; text-decoration: underline; display: inline-flex; } .${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}-hover-display { display: none; } .${ns}-hover-trigger:hover .${ns}-hover-display { display: flex; } [\\\@pointdrag], .no-touch-action { touch-action: none; } .${ns}-truncate-text { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .${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; } .${ns}-player { display: flex; flex-direction: column; flex-grow: 1; overflow: auto; } .${ns}-player .${ns}-hover-image { position: fixed; max-height: 125px; max-width: 125px; z-index: 2; } .${ns}-player.${ns}-hide-hover-image .${ns}-hover-image { display: none !important; } .${ns}-under-image { flex-grow: 1; overflow: auto; } .${ns}-list-item { list-style-type: none; padding: 0.15rem 0.25rem; white-space: nowrap; text-overflow: ellipsis; cursor: pointer; background: var(--fcsp-odd-row); overflow: hidden; height: 1.3rem; } .${ns}-list-item:nth-child(2n) { background: var(--fcsp-even-row); } .${ns}-list-item:nth-child(n).playing { background: var(--fcsp-playing); } .${ns}-list-item .${ns}-item-menu-button { right: 0.25rem; } .${ns}-list-item.${ns}-dragging { background: var(--fcsp-dragging); } .dialog .tags-dialog .entry { cursor: initial; } .${ns}-settings textarea { border: solid 1px var(--fcsp-border); min-width: 100%; min-height: 4rem; box-sizing: border-box; white-space: pre; } .${ns}-settings .${ns}-sub-settings .${ns}-heading { font-weight: normal; margin: 0; justify-content: space-between; } .${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 { justify-content: space-between; flex-direction: row-reverse; margin: 0 -0.25rem; } .${ns}-settings .${ns}-settings-tab-group { text-align: center; white-space: nowrap; align-items: center; } .${ns}-settings .${ns}-settings-tab { display: flex; margin: 0.25rem; text-decoration: underline; text-align: center; } .${ns}-settings .${ns}-settings-tab.active { font-weight: bold; } .${ns}-settings .${ns}-settings-tab svg.bi { margin: 0 -0.125rem; } .${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 var(--fcsp-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}-theme-save-options { position: fixed; max-height: 20rem; overflow: auto; padding: 0.25rem; } .${ns}-theme-save-options:not(.fields-collapsed) .bi-chevron-down, .${ns}-theme-save-options.fields-collapsed .bi-chevron-up, .${ns}-theme-save-options.fields-collapsed input[type=checkbox]:not(:checked), .${ns}-theme-save-options.fields-collapsed input[type=checkbox]:not(:checked) + label { display: none; } .${ns}-info-circle { cursor: pointer; } .${ns}-info-circle svg { height: 0.8em; width: 0.8em; } .${ns}-popover-body { position: fixed; padding: 0.25rem; max-width: 12rem; text-align: center; border-radius: 0.3rem; } .${ns}-popover-body.wide { max-width: 20rem; } pre.option { display: inline; background: #f5f5f5; border: 1px solid #b7c5d9; border-radius: 4px; padding: 0 0.3em; font-size: 0.9em; white-space: nowrap; } .${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 var(--fcsp-border); } .${ns}-threads .${ns}-thread-list .boardBanner { margin: 1rem 0; } .${ns}-threads table { border-top: solid 1px var(--fcsp-border); width: 100%; margin-top: 0.5rem; border-collapse: collapse; } .${ns}-threads table th { border-bottom: solid 1px var(--fcsp-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: var(--fcsp-even-row); } .${ns}-threads table .${ns}-threads-body tr:nth-child(2n) { background: var(--fcsp-odd-row); } .${ns}-download-all-status, .${ns}-create-sound-status { margin-top: 0.5rem; border: solid 1px var(--fcsp-border); border-radius: 5px; padding: 0.25rem; } .${ns}-file-overlay, .${ns}-tools input[type=text] { display: flex; align-items: center; } .${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}-tools .${ns}-full-bar { background: none; height: 5px; } .${ns}-tools .${ns}-full-bar > div { background: var(--fcsp-border); } .${ns}-threads, .${ns}-settings, .${ns}-tools, .${ns}-player, .fullscreen-details { 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: flex; } #${ns}-container[data-view-style=image] .${ns}-under-image { display: none; } #${ns}-container[data-view-style=image] .${ns}-image-link { height: auto; } #${ns}-container[data-view-style=playlist] .${ns}-image-link { flex-shrink: 0; flex-grow: 0; } #${ns}-container[data-view-style=fullscreen] .${ns}-player { scrollbar-width: none; } #${ns}-container[data-view-style=fullscreen] .${ns}-player::-webkit-scrollbar { display: none; } #${ns}-container[data-view-style=fullscreen].cursor-inactive .${ns}-player { cursor: none; } #${ns}-container[data-view-style=fullscreen].cursor-inactive .${ns}-controls, #${ns}-container[data-view-style=fullscreen].cursor-inactive .fullscreen-details { opacity: 0; } #${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: 0; opacity: 0.7; transition: opacity 0.3s ease; z-index: 2; } #${ns}-container[data-view-style=fullscreen] .${ns}-under-image { position: absolute; top: 100%; width: 100%; } #${ns}-container[data-view-style=fullscreen] .fullscreen-details { display: block; opacity: 0.7; position: absolute; top: 0.5rem; left: 0.5rem; text-align: left; background: var(--fcsp-controls-background); color: var(--fcsp-controls-inactive); padding: 0.5rem 1rem; font-size: 1.5rem; line-height: 1.5rem; border-radius: 0.375rem; z-index: 2; }` /***/ }), /***/ "./src/migrations.js": /*!***************************!*\ !*** ./src/migrations.js ***! \***************************/ /***/ ((module) => { // Migrations must return { [prop]: [ previous, updated ], ... } 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; }); return {}; } }, { version: '3.3.0', name: 'sound-name-title-swap', async run() { const config = Player.config; const changes = {}; const templates = [ 'headerTemplate', 'rowTemplate', 'footerTemplate', 'chanXTemplate', 'customCSS' ]; templates.forEach(prop => { /sound-name/.test(config[prop]) && (changes[prop] = [ config[prop], config[prop] = config[prop].replace(/sound-name/g, 'sound-title') ]); }); return changes; } }, { version: '3.4.0', name: 'disable-inline-player-for-existing-users', async run() { Player.config.playExpandedImages = false; Player.config.playHoveredImages = false; return { playExpandedImages: [ true, false ], playHoveredImages: [ true, false ] }; } }, { version: '3.4.7', name: 'zz-ht-to-zz-fo', async run() { const original = [ ...Player.config.allow ]; Player.config.allow.push('zz.fo'); return { allow: [ original, Player.config.allow ] } } } ]; /***/ }), /***/ "./src/player.js": /*!***********************!*\ !*** ./src/player.js ***! \***********************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var _ = __webpack_require__(/*! ./src/_ */ "./src/_.js"); const components = { // Settings must be first. settings: __webpack_require__(/*! ./components/settings */ "./src/components/settings/index.js"), events: __webpack_require__(/*! ./components/events */ "./src/components/events/index.js"), actions: __webpack_require__(/*! ./components/actions */ "./src/components/actions/index.js"), colorpicker: __webpack_require__(/*! ./components/settings/colorpicker */ "./src/components/settings/colorpicker.js"), controls: __webpack_require__(/*! ./components/controls */ "./src/components/controls/index.js"), display: __webpack_require__(/*! ./components/display */ "./src/components/display/index.js"), footer: __webpack_require__(/*! ./components/footer */ "./src/components/footer/index.js"), header: __webpack_require__(/*! ./components/header */ "./src/components/header/index.js"), hotkeys: __webpack_require__(/*! ./components/hotkeys */ "./src/components/hotkeys/index.js"), inline: __webpack_require__(/*! ./components/inline */ "./src/components/inline/index.js"), minimised: __webpack_require__(/*! ./components/minimised */ "./src/components/minimised/index.js"), playlist: __webpack_require__(/*! ./components/playlist */ "./src/components/playlist/index.js"), position: __webpack_require__(/*! ./components/position */ "./src/components/position/index.js"), posts: __webpack_require__(/*! ./components/posts */ "./src/components/posts/index.js"), theme: __webpack_require__(/*! ./components/theme */ "./src/components/theme/index.js"), threads: __webpack_require__(/*! ./components/threads */ "./src/components/threads/index.js"), tools: __webpack_require__(/*! ./components/tools */ "./src/components/tools/index.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, // Store a ref to the components so they can be iterated. components, audio: new Audio(), sounds: [], filteredSounds: [], 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), /** * Set up the player. */ async initialize() { if (Player.initialized) { return; } Player.initialized = true; try { Player.audio.dataset.id = 'main'; 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. await Player.display.render(); // Expose some functionality via PlayerEvent custom events. document.addEventListener('PlayerEvent', e => { if (e.detail.action && ( true || 0)) { return _.get(Player, e.detail.action).apply(window, e.detail.arguments); } }); } catch (err) { Player.logError('There was an error initializing 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(handler) { return typeof handler === 'string' ? _.get(Player, handler) : handler; }, /** * Compare two ids for sorting. */ compareIds(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. */ disallowedSound({ src, imageMD5 }) { try { const link = new URL(src); src = src.replace(/^(https?:)?\/\//, ''); const host = link.hostname.toLowerCase(); const result = { }; result.host = !Player.config.allow.find(h => host === h || host.endsWith('.' + h)) && host; for (let filter of Player.config.filters) { result.image = result.image || filter === imageMD5 && imageMD5; result.sound = result.sound || filter.replace(/^(https?:)?\/\//, '') === src && src; if (result.image && result.sound) { break; } } return result.host || result.image || result.sound ? result : false; } catch (err) { return { invalid: true }; } }, /** * 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(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(content, type = 'info', lifetime = 5) { if (isChanX) { content = _.element(`${content} Player[k] = Player[name][k]); (Player[name].public || []).forEach(k => { Player._public.push((Player[name].atRoot || []).includes(k) ? k : `${name}.${k}`); }); } /***/ }), /***/ "./src/selectors.js": /*!**************************!*\ !*** ./src/selectors.js ***! \**************************/ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { /* provided dependency */ var Icons = __webpack_require__(/*! ./src/icons */ "./src/icons.js"); 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', expandedImage: isChanX ? '.full-image' : '.expanded-thumb, .expandedWebm', hoverImage: isChanX ? '#ihover' : '#image-hover', playLink: { class: '', text: Icons.playFill, relative: '.fileText a', position: 'afterend', prependText: ' ', unfilterText: Icons.filter }, // 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: 'btnr', text: 'Play', relative: '.post_controls', position: 'beforeend', unfilterText: 'Add' }, 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: '', text: 'play', relative: 'br:nth-of-type(2)', position: 'beforebegin', prependText: ' [', appendText: ']', unfilterText: 'add' }, styleFetcher: 'reply style-fetcher', limitWidthOf: '.content > div, .content > table' } }[Site]; /***/ }), /***/ "./src/xhr-replace.js": /*!****************************!*\ !*** ./src/xhr-replace.js ***! \****************************/ /***/ ((module) => { function toGM() { XMLHttpRequest = xhrGM; }; function toNative() { XMLHttpRequest = xhrNative; }; const xhrNative = XMLHttpRequest; const xhrGM = function() { let method, url, headers = {}, mime; this.open = (m, u) => { method = m; url = u; }; this.setRequestHeader = (name, value) => headers[name] = value; this.getAllResponseHeaders = () => this.responseHeaders; this.getResponseHeader = name => this._responseHeaders[name.toLowerCase()]; this.overrideMimeType = m => mime = m; this.send = data => { GM.xmlHttpRequest({ method, url, headers, data, responseType: this.responseType, onload: data => { Object.assign(this, data); this._responseHeaders = (data.responseHeaders || '').split('\n').reduce((headers, h) => { let [ name, ...val ] = h.split(': '); headers[name.toLowerCase()] = val.join(': '); return headers; }, {}); this.responseText = data.responseText; this.onload(this) }, onerror: this.onerror, onreadystatechange: this.onreadystatechange, ontimeout: this.ontimeout, timeout: this.timeout, overrideMimeType: mime }); }; this.onload = null; return this; }; module.exports.GM = xhrGM; module.exports.native = xhrNative; module.exports.toGM = toGM; module.exports.toNative = toNative; /***/ }) /******/ }); /************************************************************************/ /******/ // The module cache /******/ var __webpack_module_cache__ = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ var cachedModule = __webpack_module_cache__[moduleId]; /******/ if (cachedModule !== undefined) { /******/ return cachedModule.exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = __webpack_module_cache__[moduleId] = { /******/ // no module.id needed /******/ // no module.loaded needed /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ (() => { /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ /******/ /* webpack/runtime/make namespace object */ /******/ (() => { /******/ // define __esModule on exports /******/ __webpack_require__.r = (exports) => { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ })(); /******/ /************************************************************************/ var __webpack_exports__ = {}; // This entry need to be wrapped in an IIFE because it need to be in strict mode. (() => { "use strict"; /*!*********************!*\ !*** ./src/main.js ***! \*********************/ 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"); await Player.initialize(); Player.posts.addPosts(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) { Player.posts.addPosts(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. 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); })(); /******/ })() ;