// ==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(``, 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) =>
``
).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 = {}) => `
${Player.playlist.template(data)}
${Player.settings.template(data)}
${Player.threads.template(data)}
${Player.tools.template(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
})}
`
/***/ }),
/***/ "./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 = {}) => ``
/***/ }),
/***/ "./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 ]) => `
`).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 = {}) => `
${Object.keys(data.setting.options).map(k => `
${data.setting.options[k]}
`).join('')}
`
/***/ }),
/***/ "./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.dismissTextId
? `
${Player.display.ifNotDismissed(
data.dismissTextId,
data.dismissRestoreText,
`
`
)}
`
: 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
? `
${parent ? `${parent.themeFieldTitle || parent.title}: ` : ''}${s.title}
`
: '';
}).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 = {}) => `
${Player.config.savedThemesOrder.map((name, i) => `
`).join('')}
`
/***/ }),
/***/ "./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 => `
`).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 ? '' : `
/${board.board}/
`
}).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.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 = {}) => ``
/***/ }),
/***/ "./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
`
/***/ }),
/***/ "./src/components/tools/templates/hosts-select.tpl":
/*!*********************************************************!*\
!*** ./src/components/tools/templates/hosts-select.tpl ***!
\*********************************************************/
/***/ ((module) => {
module.exports = (data = {}) => `
${Object.keys(Player.config.uploadHosts).map((hostId, i) =>
Player.config.uploadHosts[hostId] && !Player.config.uploadHosts[hostId].invalid
? `${hostId} `
: ''
).join('')}
`
/***/ }),
/***/ "./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);
})();
/******/ })()
;