8chan sounds player

Play that faggy music weeb boi

  1. // ==UserScript==
  2. // @name 8chan sounds player
  3. // @version 2.3.0_0036
  4. // @namespace 8chanss
  5. // @description Play that faggy music weeb boi
  6. // @author original by: RCC; ported to 8chan by: soundboy_1459944
  7. // @website https://greasyfork.org/en/scripts/533468-8chan-sounds-player
  8. // @match https://8chan.moe/*/res/*
  9. // @match https://8chan.se/*/res/*
  10. // @match https://8chan.moe/*/last/*
  11. // @match https://8chan.se/*/last/*
  12. // @connect 4chan.org
  13. // @connect 4channel.org
  14. // @connect a.4cdn.org
  15. // @connect 8chan.moe
  16. // @connect 8chan.se
  17. // @connect desu-usergeneratedcontent.xyz
  18. // @connect arch-img.b4k.co
  19. // @connect archive-media-0.nyafuu.org
  20. // @connect 4cdn.org
  21. // @connect a.pomf.cat
  22. // @connect pomf.cat
  23. // @connect litter.catbox.moe
  24. // @connect files.catbox.moe
  25. // @connect catbox.moe
  26. // @connect share.dmca.gripe
  27. // @connect z.zz.ht
  28. // @connect z.zz.fo
  29. // @connect zz.ht
  30. // @connect too.lewd.se
  31. // @connect lewd.se
  32. // @connect *
  33. // @grant GM.getValue
  34. // @grant GM.setValue
  35. // @grant GM.xmlHttpRequest
  36. // @grant GM_addValueChangeListener
  37. // @grant GM_getResourceURL
  38. // @grant GM_addElement
  39. // @run-at document-start
  40. // @license CC0 1.0
  41. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwAAADsABataJCQAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMS42/U4J6AAAAs9JREFUaEPdmlFu20AMRIX2FL56c73mDgWM1GWXjwI9srQro5apPsxHlhyOPchPPjJ9//g8l779+Gman+crIPofC0wJWRVUp4Ah26dF3AKxmVisoGZ5mzDeI569ImWFcaeRzX/98jZhXCC2XSJinSec+OVtwvgIcY6L+3WecOKXtwnjNN1uN35KiHlEXAYWO8PoUQEcAdO9BfKlI+YRcdnwzBmmLy3gMGqIvyvOFt/JYHFAAYNpQ062xc3bCxgsGnK1IQ6qFXDk9qGwVijgsA7kfCl8ZQsYkiDCVKeAgSMhIVk4ShVw8AWSM4t1wQIG1kCiXOxqFnA4aEiaiUXlAgY3wWAgiwoFDM6CkUAWRQo4HDc88HK5+BNHwudG99OZHlDA4L5hgVbAO7BOuMfofjrTYwoYRATegV2CdcECBimNUxYwCGqcsoBBVsA0YFq5gEFcg1HAtHgBg8QGowaj+gUMQhuMzlXAIPe8BQyiI5zH6X4Dhky6n870jQUIbTA6SwHiAqYNRpULkBUwDZiWLUBQwDTBomYBUoKT/S1ERND+GD1JAY4DC/Rvbz/jSLjH6H4601cX4DLwQP/2BqaEz43upzN9aQHOgpFAFv+wAK4Gp2NwEzzMxJpgUa3AWibWBIsXFbh+cdyFg4YEmlgcXMBuBgtw0JA0F7uaBXAHkuZid3ABg9MVMCUkahbrUgVwJCQnC8cxBUwY1wuwTkiCCFORAuwScr4UvoMLcHePrzJy+1BYjymAa/PDHDncEAcDBXg0cARMdxW4fv32Y4dFQ662xc36dzKyzcEUMN1VIMO0ISddcfauArwTctIVZ9P068qvlHewZpth0ZhjudJ3sLx0xD8iLuOb8UgsbVlMgzmWK31vIuZBcbzOoM2YM2ftKCDOXSLiHvGYWKwgZtdoAbHtFSkJMcxivUBss/oFxFBN1b9fV5P/N3v+h/Yz6ePzD965YtNhR5uIAAAAAElFTkSuQmCC
  42. // ==/UserScript==
  43.  
  44. //kudos to the original sound player by RCC: https://github.com/rcc11/4chan-sounds-player
  45.  
  46. (function(modules) { // webpackBootstrap
  47. 'use strict';
  48.  
  49. // The module cache
  50. var installedModules = {};
  51.  
  52. // The require function
  53. function __webpack_require__(moduleId) {
  54.  
  55. // Check if module is in cache
  56. if (installedModules[moduleId]) {
  57. return installedModules[moduleId].exports;
  58. }
  59. // Create a new module (and put it into the cache)
  60. var module = installedModules[moduleId] = {
  61. i: moduleId,
  62. l: false,
  63. exports: {}
  64. };
  65.  
  66. // Execute the module function
  67. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  68.  
  69. // Flag the module as loaded
  70. module.l = true;
  71.  
  72. // Return the exports of the module
  73. return module.exports;
  74. }
  75.  
  76.  
  77. // expose the modules object (__webpack_modules__)
  78. __webpack_require__.m = modules;
  79.  
  80. // expose the module cache
  81. __webpack_require__.c = installedModules;
  82.  
  83. // define getter function for harmony exports
  84. __webpack_require__.d = function(exports, name, getter) {
  85. if (!__webpack_require__.o(exports, name)) {
  86. Object.defineProperty(exports, name, {
  87. enumerable: true,
  88. get: getter
  89. });
  90. }
  91. };
  92.  
  93. // define __esModule on exports
  94. __webpack_require__.r = function(exports) {
  95. if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
  96. Object.defineProperty(exports, Symbol.toStringTag, {
  97. value: 'Module'
  98. });
  99. }
  100. Object.defineProperty(exports, '__esModule', {
  101. value: true
  102. });
  103. };
  104.  
  105. // create a fake namespace object
  106. // mode & 1: value is a module id, require it
  107. // mode & 2: merge all properties of value into the ns
  108. // mode & 4: return value when already ns object
  109. // mode & 8|1: behave like require
  110. __webpack_require__.t = function(value, mode) {
  111. if (mode & 1) value = __webpack_require__(value);
  112. if (mode & 8) return value;
  113. if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
  114. var ns = Object.create(null);
  115. __webpack_require__.r(ns);
  116. Object.defineProperty(ns, 'default', {
  117. enumerable: true,
  118. value: value
  119. });
  120. if (mode & 2 && typeof value != 'string')
  121. for (var key in value) __webpack_require__.d(ns, key, function(key) {
  122. return value[key];
  123. }.bind(null, key));
  124. return ns;
  125. };
  126.  
  127. // getDefaultExport function for compatibility with non-harmony modules
  128. __webpack_require__.n = function(module) {
  129. var getter = module && module.__esModule ?
  130. function getDefault() {
  131. return module['default'];
  132. } :
  133. function getModuleExports() {
  134. return module;
  135. };
  136. __webpack_require__.d(getter, 'a', getter);
  137. return getter;
  138. };
  139.  
  140. // Object.prototype.hasOwnProperty.call
  141. __webpack_require__.o = function(object, property) {
  142. return Object.prototype.hasOwnProperty.call(object, property);
  143. };
  144.  
  145. // __webpack_public_path__
  146. __webpack_require__.p = "";
  147.  
  148. // Load entry module and return exports
  149. return __webpack_require__(__webpack_require__.s = 3);
  150. })
  151. ([
  152. /* 0 - File Parser
  153. • parseFileName(): Extracts sound URLs from filenames using regex pattern [sound=URL]
  154. • parsePost(): Processes individual posts to find sound files and create play buttons
  155. • parseFiles(): Scans the page or specific elements for posts containing sounds
  156. • Key Features:
  157. o Handles URL decoding
  158. o Creates unique IDs for each sound
  159. o Generates play links next to sound files
  160. */
  161. (function(module, exports) {
  162. const protocolRE = /^(https?:)?\/\//;
  163. const filenameRE = /(.*?)[[({](?:audio|sound)[ =:|$](.*?)[\])}]/gi;
  164. const filenameRE2 = /(\[([^\]]*(?:catbox\.moe)[^\]]*)\])/gi;
  165. const videoFileExtRE = /\.(webm|mp4|m4v|ogv|avi|mpeg|mpg|mpe|m1v|m2v|mov|wmv)$/i;
  166. const audioFileExtRE = /\.(mp3|m4a|m4b|flac|ogg|oga|opus|mp2|mpega|wav|aac)$/i;
  167. const imageMimeRE = /^image\/.+$/;
  168. const videoMimeRE = /^video\/.+$/;
  169. const audioMimeRE = /^audio\/.+$/;
  170. //const playlistExtRE = /\.(m3u|asx)$/i;
  171.  
  172. // Function to safely get file extension (handles multiple dots in filename)
  173. function getFileExtension(filename) {
  174. // Handle edge cases: no extension, hidden files, or filenames ending with dot
  175. if (!filename || filename.indexOf('.') === -1 || filename.endsWith('.')) {
  176. return '';
  177. }
  178. return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase();
  179. }
  180.  
  181. function determinateMimeType(extension, isVideo, isAudio) {
  182. let type;
  183. if (isVideo) {
  184. switch (extension) {
  185. case 'webm': type = 'video/webm'; break;
  186. case 'mp4': type = 'video/mp4'; break;
  187. case 'm4v': type = 'video/mp4'; break;
  188. case 'ogv': type = 'video/ogg'; break;
  189. case 'avi': type = 'video/x-msvideo'; break;
  190. case 'mpeg': case 'mpg': case 'mpe': case 'm1v': case 'm2v':
  191. type = 'video/mpeg'; break;
  192. default: type = 'video/mp4'; // default fallback
  193. }
  194. } else if (isAudio) {
  195. switch (extension) {
  196. case 'mp3': case 'mpega': case 'mp2': type = 'audio/mpeg'; break;
  197. case 'm4a': case 'm4b': type = 'audio/mp4'; break;
  198. case 'flac': type = 'audio/flac'; break;
  199. case 'ogg': case 'oga': case 'opus': type = 'audio/ogg'; break;
  200. case 'wav': type = 'audio/wav'; break;
  201. case 'aac': type = 'audio/aac'; break;
  202. default: type = 'audio/mpeg'; // default fallback
  203. }
  204. } else {
  205. type = 'audio/mpeg'; // ultimate fallback
  206. }
  207. return type;
  208. }
  209.  
  210. function getFullFilename(element) {
  211. if (element.dataset.fileExt) {
  212. return element.textContent + element.nextElementSibling.textContent;
  213. }
  214. return element.textContent;
  215. }
  216.  
  217. function formatFileTitle(postId, fileIndex, fileSize, filename) {
  218. // Extract file extension
  219. const fileExt = filename.split('.').pop().toLowerCase();
  220. // Get base filename without extension
  221. let baseName = filename.replace(/\.[^/.]+$/, "");
  222. if(/^[a-z0-9]{64}$/.test(baseName)) baseName = `<span style="opacity: 0.8; background: transparent !important">${baseName.slice(0, 8)}</span>`; // If the filename is randomly generated text, shorten it.
  223.  
  224. // local files case (module 13 addFromFiles())
  225. if(fileSize == null) return `locF:${localFileCounter} &nbsp; <span class="${ns}-playlist-file-ext">.${fileExt}</span>&nbsp;${baseName}`;
  226.  
  227. const displaySize = formatFileSize(fileSize, true);
  228.  
  229. return `${postId} &nbsp; ${displaySize} <span class="${ns}-playlist-file-ext">.${fileExt}</span>&nbsp;${baseName}`;
  230. }
  231.  
  232. function formatFileTitle2(postId, fileIndex, fileSize, filename) {
  233. // Extract file extension
  234. const fileExt = filename.split('.').pop().toLowerCase();
  235. // Get base filename without extension
  236. let baseName = filename.replace(/\.[^/.]+$/, "");
  237. if(/^[a-z0-9]{64}$/.test(baseName)) baseName = `${baseName.slice(0, 8)}`; // If the filename is randomly generated text, shorten it.
  238.  
  239. // local files case (module 13 addFromFiles())
  240. if(fileSize == null) return `locF:${localFileCounter} &nbsp; .${fileExt}&nbsp;${baseName}`;
  241.  
  242. const displaySize = formatFileSize(fileSize, false);
  243.  
  244. return `${postId} &nbsp; ${displaySize} .${fileExt}&nbsp;${baseName}`;
  245. }
  246.  
  247. function formatFileSize(fileSize, addSpace = false) {
  248. // local files case (module 13 addFromFiles())
  249. if(fileSize == null) return 'NULL';
  250.  
  251. // Convert fileSize (assumed to be a string like "99.50 KB" or "1.82 MB") into MB
  252. let sizeValue = parseFloat(fileSize);
  253. let sizeInMB = 0;
  254.  
  255. if (fileSize.toLowerCase().includes("kb")) {
  256. sizeInMB = sizeValue / 1024;
  257. } else if (fileSize.toLowerCase().includes("mb")) {
  258. sizeInMB = sizeValue;
  259. }
  260.  
  261. // Round up to 1 decimal place
  262. sizeInMB = Math.ceil(sizeInMB * 10) / 10;
  263.  
  264. // Cap anything over 99.5 MB
  265. // Omit the .0 when it's a 2 digits number like 11.0 MB (11.0 MB → 11 MB).
  266. let displaySize = sizeInMB > 99 ? "99+ MB" : `${(sizeInMB > 9.9 ? '&puncsp;' + sizeInMB.toFixed(0): sizeInMB.toFixed(1))} MB`;
  267. if(!addSpace) displaySize = sizeInMB > 99 ? "99+ MB" : `${(sizeInMB > 9.9 ? sizeInMB.toFixed(0): sizeInMB.toFixed(1))} MB`;
  268.  
  269. return displaySize;
  270. }
  271.  
  272.  
  273. function getPostNumber(postElement) {
  274. // If not found in ID, look for the linkQuote element
  275. const linkQuote = postElement.querySelector('.linkQuote');
  276. if (linkQuote && linkQuote.textContent && /^\d+$/.test(linkQuote.textContent)) {
  277. return linkQuote.textContent;
  278. }
  279.  
  280. // Fallback to a generated ID if nothing else works
  281. return 'idGrabFailed';
  282. }
  283.  
  284. function parseFileName(filename, image, post, thumb, imageMD5, fileIndex, fileSize, dataFilemime, width = null, height = null) {
  285. if (!filename) return [];
  286. filename = filename.replace(/-/, '/');
  287.  
  288. // First check for [sound=URL] tags
  289. const matches = [];
  290. let match;
  291. while (((match = filenameRE.exec(filename)) !== null) || (((match = filenameRE2.exec(filename)) !== null))) {
  292. matches.push(match);
  293. }
  294.  
  295. // If we found sound tags, process them and ignore video files
  296. if (matches.length) {
  297. return matches.reduce((sounds, match, i) => {
  298. let src = match[2];
  299. const id = post + ':' + fileIndex;
  300. //const title = match[1].trim() || defaultName + (matches.length > 1 ? ` (${i + 1})` : '');
  301.  
  302. try {
  303. if (src.includes('_') && !src.includes('%')) src = src.replace(/_/g, '%'); // Fix for Firefox issue: replace underscores that were mistakenly used instead of percent-encoding
  304. if (src.includes('%')) src = decodeURIComponent(src);
  305. if (src.match(protocolRE) === null) src = (location.protocol + '//' + src);
  306. } catch (error) {
  307. return sounds;
  308. }
  309.  
  310. // Determine if this is a video file based on extension
  311. const isVideo = videoFileExtRE.test(src) ? true : false;
  312. const isAudio = audioFileExtRE.test(src) ? true : false;
  313.  
  314. // Determine the MIME type based on extension
  315. const extension = getFileExtension(src);
  316. let type = determinateMimeType(extension, isVideo, isAudio)
  317.  
  318. const sound = {
  319. src, // external sound
  320. id,
  321. title: formatFileTitle(post, fileIndex, fileSize, filename),
  322. title2: formatFileTitle2(post, fileIndex, fileSize, filename),
  323. post,
  324. image, // image or video taked from the post
  325. filename,
  326. thumb,
  327. imageMD5,
  328. type, // external sound
  329. isVideo, // is external sound video?
  330. hasSoundTag: true,
  331. fileIndex: fileIndex,
  332. fileSize: formatFileSize(fileSize, false),
  333. dataFilemime: dataFilemime,
  334. width,
  335. height
  336. };
  337. Player.acceptedSound(sound) && sounds.push(sound);
  338. return sounds;
  339. }, []);
  340. }
  341.  
  342. // If no sound tags found, check for video files
  343. const isVideo = videoMimeRE.test(dataFilemime);
  344. const isAudio = audioMimeRE.test(dataFilemime);
  345. if (isVideo || isAudio) {
  346. const id = post + ':' + fileIndex + ':0';
  347.  
  348. // Determine the MIME type based on extension
  349. const extension = getFileExtension(image);
  350. let type = determinateMimeType(extension, isVideo, false)
  351.  
  352. return [{
  353. src: image, // Use the image URL as src for video files
  354. id: post + ':' + fileIndex,
  355. title: formatFileTitle(post, fileIndex, fileSize, filename),
  356. title2: formatFileTitle2(post, fileIndex, fileSize, filename),
  357. post,
  358. image, // image (post file)
  359. filename,
  360. thumb,
  361. imageMD5,
  362. type, // image (post file)
  363. isVideo, // is image (post file) video?
  364. hasSoundTag: false, // external sound
  365. fileIndex: fileIndex,
  366. fileSize: formatFileSize(fileSize, false),
  367. dataFilemime: dataFilemime,
  368. width,
  369. height
  370. }];
  371. }
  372.  
  373. return [];
  374. }
  375.  
  376. function parsePost(post, skipRender) {
  377. try {
  378. // Get the actual post number for this post
  379. const postNumber = getPostNumber(post);
  380. if (!postNumber) return;
  381.  
  382. // If there are existing play links, just reconnect their handlers
  383. const existingLinks = post.querySelectorAll(`.${ns}-play-link`);
  384. if (existingLinks.length > 0) {
  385. existingLinks.forEach(link => {
  386. const id = link.getAttribute('data-id');
  387. link.onclick = () => Player.play(Player.sounds.find(sound => sound.id === id));
  388. });
  389. return;
  390. }
  391.  
  392. // Get all file containers in the post
  393. const fileContainers = post.querySelectorAll('.uploadCell');
  394. if (!fileContainers || fileContainers.length === 0) return;
  395.  
  396. let allSounds = [];
  397.  
  398. // Process each file in the post
  399. fileContainers.forEach((container, fileIndex) => {
  400. let filename = null;
  401. let fileLink = null;
  402. let fileSize = "0 KB";
  403.  
  404. // Try to get filename from various locations
  405. const originalNameLink = container.querySelector('.originalNameLink');
  406. if (originalNameLink) filename = getFullFilename(originalNameLink);
  407.  
  408. // Get file size if available
  409. const sizeLabel = container.querySelector('.sizeLabel');
  410. if (sizeLabel) fileSize = sizeLabel.textContent.trim();
  411.  
  412. // Get file dimensions if available
  413. const dimensionLabel = container.querySelector('.dimensionLabel'); // e.g. '123x123'
  414.  
  415. // If no filename found via standard selectors, try to find file links
  416. if (!filename) {
  417. const fileLinkEl = container.querySelector('.nameLink');
  418. if (fileLinkEl) {
  419. fileLink = fileLinkEl.href;
  420. filename = fileLink.split('/').pop();
  421. }
  422. }
  423.  
  424. if (!filename) return;
  425.  
  426. const fileThumb = container.querySelector('.imgLink');
  427. const imageSrc = fileThumb && fileThumb.href;
  428. const thumbImg = fileThumb && fileThumb.querySelector('img');
  429. let thumbSrc = thumbImg && thumbImg.src;
  430. const md5Match = imageSrc && imageSrc.match(/\/\.media\/([a-f0-9]{64})/i);
  431. const imageMD5 = md5Match && md5Match[1];
  432. const dataFilemime = fileThumb && fileThumb.getAttribute('data-filemime');
  433. let dimensions = [null, null];
  434. if(dimensionLabel) {
  435. dimensions = dimensionLabel.textContent.trim().split(/x|×/);
  436. } else {
  437. const img = new Image();
  438. img.onload = function() {
  439. dimensions = [img.width, img.height];
  440. }
  441. img.src = thumbImg.src;
  442. }
  443.  
  444. // Replace spoiler thumbnail with actual thumbnail if available
  445. if (/spoiler/.test(thumbImg.src)) {
  446. const domain = new URL(thumbImg.src).origin;
  447. thumbSrc = imageSrc && `${domain}/.media/t_${imageMD5}`;
  448. }
  449.  
  450. // Set the full image as the thumbnail for images that are 220x220 pixels or smaller.
  451. // This is a fix for small images because thumbnails are not generated for them.
  452. // This crap does not apply to GIFs, GIFs always generate thumbnails.
  453. if (dimensionLabel && /^image\/.+$/.test(dataFilemime) && !/^image\/gif$/.test(dataFilemime) ) {
  454. if (dimensions.length === 2) {
  455. const width = parseInt(dimensions[0]);
  456. const height = parseInt(dimensions[1]);
  457. if (width <= 220 && height <= 220) {
  458. thumbSrc = imageSrc;
  459. }
  460. }
  461. }
  462.  
  463. const sounds = parseFileName(filename, imageSrc, postNumber, thumbSrc, imageMD5, fileIndex, fileSize, dataFilemime, parseInt(dimensions[0]), parseInt(dimensions[1]));
  464. if (!sounds.length) return;
  465.  
  466. allSounds = allSounds.concat(sounds);
  467.  
  468. // Create play link for this file
  469. const firstID = sounds[0].id;
  470. const text = '▶︎';
  471. const clss = `${ns}-play-link`;
  472. let playLinkParent = container.querySelector('.uploadDetails') ||
  473. container.querySelector('.fileLink') ||
  474. container.querySelector('.fileText') ||
  475. container; // Fallback to the container itself
  476.  
  477. if (playLinkParent) {
  478. const playLink = document.createElement('a');
  479. playLink.href = "javascript:;";
  480. playLink.className = clss;
  481. playLink.setAttribute('data-id', firstID);
  482. playLink.textContent = text;
  483. playLink.title = 'play';
  484. playLink.style.display = 'inline-block'; // Ensure the link is displayed inline
  485. playLink.style.marginLeft = '3px'; // Add some spacing
  486. playLink.onclick = () => Player.play(Player.sounds.find(sound => sound.id === firstID));
  487.  
  488. playLinkParent.appendChild(document.createTextNode(' '));
  489. playLinkParent.appendChild(playLink);
  490. }
  491. });
  492.  
  493. if (allSounds.length === 0) return;
  494.  
  495. allSounds.forEach(sound => Player.add(sound, skipRender));
  496. return allSounds.length > 0;
  497. } catch (err) {
  498. console.error('[8chan sounds player] Error parsing post:', err);
  499. console.error(post);
  500. }
  501. }
  502.  
  503. function parseFiles(target, postRender) {
  504. let addedSounds = false;
  505. let posts = target.classList && target.classList.contains('postCell') ?
  506. [target] :
  507. target.querySelectorAll('.innerOP, .innerPost');
  508.  
  509. posts.forEach(post => parsePost(post, postRender) && (addedSounds = true));
  510.  
  511. if (addedSounds && postRender && Player.container) Player.playlist.render();
  512. }
  513.  
  514. module.exports = {
  515. parseFiles,
  516. parsePost,
  517. parseFileName
  518. };
  519. }),
  520. /* 1 - Settings Configuration
  521. • Contains all default configuration options for the player:
  522. o Playback settings (shuffle, repeat)
  523. o UI settings (view styles, hover images)
  524. o Keybindings
  525. o Allowed hosts list
  526. o Color schemes
  527. o Template layouts
  528. • Defines the structure for:
  529. o Header/footer/row templates
  530. o Hotkey bindings
  531. o Player appearance settings
  532. */
  533. (function(module, exports) {
  534.  
  535. module.exports = [{
  536. property: 'shuffle',
  537. default: false
  538. },
  539. {
  540. property: 'repeat',
  541. default: 'all'
  542. },
  543. {
  544. property: 'viewStyle',
  545. default: 'gallery',
  546. options: {
  547. playlist: 'Playlist',
  548. image: 'Image',
  549. gallery: 'Gallery'
  550. }
  551. },
  552. {
  553. property: 'hoverImages',
  554. default: true
  555. },
  556. {
  557. property: 'preventHoverImagesFor',
  558. default: [],
  559. save: false
  560. },
  561. {
  562. property: 'volumeValue',
  563. title: 'Volume value',
  564. description: 'Stores the volume value from the previous session.',
  565. showInSettings: false,
  566. default: '1'
  567. },
  568. {
  569. title: 'Miscellaneous',
  570. description: 'Variety of different settings',
  571. showInSettings: true,
  572. settings: [{
  573. property: 'fontSize',
  574. title: 'Font Size',
  575. description: 'Adjust the font size.',
  576. default: '13',
  577. showInSettings: true,
  578. updateStylesheet: true,
  579. actions: [{
  580. title: 'Reset',
  581. handler: 'settings.reset'
  582. }],
  583. },
  584. {
  585. property: 'autoshow',
  586. default: true,
  587. title: 'Autoshow',
  588. description: 'Automatically show the player when the thread contains sounds.',
  589. showInSettings: false,
  590. actions: [{
  591. title: 'Reset',
  592. handler: 'settings.reset'
  593. }],
  594. },
  595. {
  596. property: 'pauseOnHide',
  597. default: false,
  598. title: 'Pause on hide',
  599. description: 'Pause the player when it\'s hidden.',
  600. showInSettings: true,
  601. actions: [{
  602. title: 'Reset',
  603. handler: 'settings.reset'
  604. }],
  605. },
  606. {
  607. property: 'showSoundTagOnly',
  608. default: false,
  609. title: '<span style="margin: 0.2em 0;">Only Show<br>Sound Posts</span>',
  610. description: 'When enabled, only posts with [sound=URL] tags will be displayed in the playlist.',
  611. showInSettings: true,
  612. actions: [{
  613. title: 'Reset',
  614. handler: 'settings.reset'
  615. }],
  616. },
  617. {
  618. property: 'borderWidth',
  619. default: '1px',
  620. title: 'Border Width',
  621. showInSettings: true,
  622. updateStylesheet: true,
  623. actions: [{
  624. title: 'Reset',
  625. handler: 'settings.forceBorderWidth',
  626. }],
  627. },
  628. ]
  629. },
  630. {
  631. title: 'Media Display Settings',
  632. description: 'Settings for media display dimensions.',
  633. showInSettings: true,
  634. settings: [{
  635. property: 'minMediaHeight',
  636. title: 'Minimum Height',
  637. description: 'Maximum width for the Media Display.',
  638. default: '25px',
  639. showInSettings: true,
  640. updateStylesheet: true,
  641. actions: [{
  642. title: 'Reset',
  643. handler: 'settings.reset'
  644. }],
  645. },
  646. {
  647. property: 'maxMediaHeight',
  648. title: 'Maximum Height',
  649. description: 'Maximum height for the Media Display.',
  650. default: '400px',
  651. showInSettings: true,
  652. updateStylesheet: true,
  653. actions: [{
  654. title: 'Reset',
  655. handler: 'settings.reset'
  656. }],
  657. },
  658. ]
  659. },
  660. {
  661. title: 'Minimised Display',
  662. description: 'Optional displays for when the player is minimised.',
  663. settings: [{
  664. property: 'pip',
  665. title: 'Enabled',
  666. description: 'Display a fixed Minimised Display of the playing sound in the bottom right of the thread.',
  667. default: true,
  668. showInSettings: true,
  669. actions: [{
  670. title: 'Reset',
  671. handler: 'settings.reset'
  672. }],
  673. },
  674. {
  675. property: 'maxPIPWidth',
  676. title: 'Maximum Width',
  677. description: 'Maximum width for the Minimised Display.',
  678. default: '200px',
  679. updateStylesheet: true,
  680. showInSettings: true,
  681. actions: [{
  682. title: 'Reset',
  683. handler: 'settings.reset'
  684. }],
  685. },
  686. {
  687. property: 'maxPIPHeight',
  688. title: 'Maximum Height',
  689. description: 'Maximum height for the Minimised Display.',
  690. default: '250px',
  691. updateStylesheet: true,
  692. showInSettings: true,
  693. actions: [{
  694. title: 'Reset',
  695. handler: 'settings.reset'
  696. }],
  697. },
  698. {
  699. property: 'offsetBottomPIP',
  700. title: 'Bottom offset',
  701. description: 'Changes the bottom offset (position) of the minimized player.',
  702. default: '10px',
  703. updateStylesheet: true,
  704. showInSettings: true,
  705. actions: [{
  706. title: 'Reset',
  707. handler: 'settings.reset'
  708. }],
  709. },
  710. {
  711. property: 'offsetRightPIP',
  712. title: 'Right offset',
  713. description: 'Changes the right offset (position) of the minimized player.',
  714. default: '10px',
  715. updateStylesheet: true,
  716. showInSettings: true,
  717. actions: [{
  718. title: 'Reset',
  719. handler: 'settings.reset'
  720. }],
  721. },
  722. {
  723. property: 'zIndexPIP',
  724. title: 'Z-Index',
  725. description: 'Changes the Z-INDEX of the minimized player. Setting the value below 0 will disable the "remaximize on-click" feature. To maximize the player again, click the icon in the header.',
  726. default: '0',
  727. updateStylesheet: true,
  728. showInSettings: true,
  729. actions: [{
  730. title: 'Reset',
  731. handler: 'settings.reset'
  732. }],
  733. },
  734. {
  735. property: 'chanXControls',
  736. title: '4chan X Header Controls',
  737. description: 'Show playback controls in the 4chan X header. Customise the template below.',
  738. showInSettings: isChanX,
  739. options: {
  740. always: 'Always',
  741. closed: 'Only with the player closed',
  742. never: 'Never'
  743. }
  744. }
  745. ]
  746. },
  747. {
  748. title: "Controls",
  749. displayGroup: "Display",
  750. showInSettings: true,
  751. settings: [{
  752. property: "preventControlWrapping",
  753. title: "Prevent Wrapping",
  754. description: "Hide elements from controls to prevent wrapping when the player is too small",
  755. default: true,
  756. actions: [{
  757. title: 'Reset',
  758. handler: 'settings.reset'
  759. }],
  760. },
  761. {
  762. property: "controlsHideOrder",
  763. title: "Hide Order",
  764. description: 'Order controls are hidden in to prevent wrapping. ' +
  765. 'Available controls are\n' +
  766. 'previous, ' +
  767. 'play, ' +
  768. 'next, ' +
  769. 'seek-bar, ' +
  770. 'time, ' +
  771. 'duration, ' +
  772. 'volume-bar ' +
  773. 'and fullscreen.',
  774. default: ["fullscreen", "seek-bar", "duration", "time", "volume-bar", "previous", "next"],
  775. showInSettings: 'textarea',
  776. attrs: 'style="height:120px;"',
  777. split: '\n',
  778. actions: [{
  779. title: 'Reset',
  780. handler: 'settings.reset'
  781. }],
  782. }]
  783. },
  784. {
  785. title: 'Limit Post Width',
  786. description: 'Limit the width of posts so they aren\'t hidden under the player.',
  787. showInSettings: true,
  788. settings: [{
  789. property: 'limitPostWidths',
  790. title: 'Enabled',
  791. default: false,
  792. actions: [{
  793. title: 'Reset',
  794. handler: 'settings.reset'
  795. }],
  796. },
  797. {
  798. property: 'minPostWidth',
  799. title: 'Minimum Width',
  800. default: '30%',
  801. actions: [{
  802. title: 'Reset',
  803. handler: 'settings.reset'
  804. }],
  805. }
  806. ]
  807. },
  808. {
  809. property: 'threadsViewStyle',
  810. title: 'Threads View',
  811. description: 'How threads in the threads view are listed.',
  812. showInSettings: false,
  813. settings: [{
  814. title: 'Display',
  815. default: 'table',
  816. options: {
  817. table: 'Table',
  818. board: 'Board'
  819. }
  820. }]
  821. },
  822. {
  823. title: 'Keybinds',
  824. showInSettings: true,
  825. description: 'Enable keyboard shortcuts.',
  826. format: 'hotkeys.stringifyKey',
  827. parse: 'hotkeys.parseKey',
  828. class: `${ns}-key-input`,
  829. property: 'hotkey_bindings',
  830. settings: [{
  831. property: 'hotkeys',
  832. default: 'open',
  833. handler: 'hotkeys.apply',
  834. title: 'Enabled',
  835. format: null,
  836. parse: null,
  837. class: null,
  838. options: {
  839. always: 'Always',
  840. open: 'Only with the player open',
  841. never: 'Never'
  842. }
  843. },
  844. {
  845. property: 'hotkey_bindings.playPause',
  846. title: 'Play/Pause',
  847. keyHandler: 'togglePlay',
  848. ignoreRepeat: true,
  849. default: {
  850. key: ' '
  851. },
  852. actions: [{
  853. title: 'R',
  854. handler: 'settings.reset'
  855. }],
  856. },
  857. {
  858. property: 'hotkey_bindings.previous',
  859. title: 'Previous',
  860. keyHandler: 'previous',
  861. ignoreRepeat: true,
  862. default: {
  863. key: 'arrowleft'
  864. },
  865. actions: [{
  866. title: 'R',
  867. handler: 'settings.reset'
  868. }],
  869. },
  870. {
  871. property: 'hotkey_bindings.next',
  872. title: 'Next',
  873. keyHandler: 'next',
  874. ignoreRepeat: true,
  875. default: {
  876. key: 'arrowright'
  877. },
  878. actions: [{
  879. title: 'R',
  880. handler: 'settings.reset'
  881. }],
  882. },
  883. {
  884. property: 'hotkey_bindings.volumeUp',
  885. title: 'Volume Up',
  886. keyHandler: 'hotkeys.volumeUp',
  887. default: {
  888. shiftKey: true,
  889. key: 'arrowup'
  890. },
  891. actions: [{
  892. title: 'R',
  893. handler: 'settings.reset'
  894. }],
  895. },
  896. {
  897. property: 'hotkey_bindings.volumeDown',
  898. title: 'Volume Down',
  899. keyHandler: 'hotkeys.volumeDown',
  900. default: {
  901. shiftKey: true,
  902. key: 'arrowdown'
  903. },
  904. actions: [{
  905. title: 'R',
  906. handler: 'settings.reset'
  907. }],
  908. },
  909. {
  910. property: 'hotkey_bindings.toggleFullscreen',
  911. title: 'Toggle Fullscreen',
  912. keyHandler: 'display.toggleFullScreen',
  913. default: {
  914. key: ''
  915. },
  916. actions: [{
  917. title: 'R',
  918. handler: 'settings.reset'
  919. }],
  920. },
  921. {
  922. property: 'hotkey_bindings.togglePlayer',
  923. title: 'Show/Hide',
  924. keyHandler: 'display.toggle',
  925. default: {
  926. key: 'h'
  927. },
  928. actions: [{
  929. title: 'R',
  930. handler: 'settings.reset'
  931. }],
  932. },
  933. {
  934. property: 'hotkey_bindings.togglePlaylist',
  935. title: 'Toggle Playlist',
  936. keyHandler: 'playlist.toggleView',
  937. default: {
  938. key: ''
  939. },
  940. actions: [{
  941. title: 'R',
  942. handler: 'settings.reset'
  943. }],
  944. },
  945. {
  946. property: 'hotkey_bindings.scrollToPlaying',
  947. title: 'Jump To Playing',
  948. keyHandler: 'playlist.scrollToPlaying',
  949. default: {
  950. key: ''
  951. },
  952. actions: [{
  953. title: 'R',
  954. handler: 'settings.reset'
  955. }],
  956. },
  957. {
  958. property: 'hotkey_bindings.toggleHoverImages',
  959. title: 'Toggle Hover Images',
  960. keyHandler: 'playlist.toggleHoverImages',
  961. default: {
  962. key: ''
  963. },
  964. actions: [{
  965. title: 'R',
  966. handler: 'settings.reset'
  967. }],
  968. }
  969. ]
  970. },
  971. {
  972. property: 'allow',
  973. title: 'Allowed Hosts',
  974. description: 'Which domains sources are allowed to be loaded from.',
  975. default: [
  976. '4cdn.org',
  977. '8chan.se',
  978. '8chan.moe',
  979. 'catbox.moe',
  980. 'dmca.gripe',
  981. 'lewd.se',
  982. 'pomf.cat',
  983. 'zz.ht'
  984. ],
  985. actions: [{
  986. title: 'Reset',
  987. handler: 'settings.reset'
  988. }],
  989. showInSettings: true,
  990. split: '\n'
  991. },
  992. {
  993. property: 'filters',
  994. default: ['# Image MD5 or sound URL'],
  995. title: 'Filters',
  996. description: 'List of URLs or image MD5s to filter, one per line.\nLines starting with a # will be ignored.',
  997. actions: [{
  998. title: 'Reset',
  999. handler: 'settings.reset'
  1000. }],
  1001. showInSettings: true,
  1002. split: '\n'
  1003. },
  1004. {
  1005. property: 'headerTemplate',
  1006. title: 'Header Contents',
  1007. actions: [{
  1008. title: 'Reset',
  1009. handler: 'settings.reset'
  1010. }],
  1011. //default: 'repeat-button shuffle-button hover-images-button playlist-button\nsound-name\nadd-button reload-button threads-button settings-button close-button',
  1012. default: 'repeat-button shuffle-button hover-images-button playlist-button &nbsp; \nsound-name &nbsp; \nadd-button reload-button settings-button &nbsp; close-button',
  1013. description: 'Template for the header contents.\n\nElements inside of p:{ &nbsp; } are hidden if no sound is playing.\nElements inside of h:{ &nbsp; } are hidden and appear on hover.',
  1014. showInSettings: 'textarea',
  1015. },
  1016. {
  1017. property: 'rowTemplate',
  1018. title: 'Row Contents',
  1019. actions: [{
  1020. title: 'Reset',
  1021. handler: 'settings.reset'
  1022. }],
  1023. default: 'sound-name h:{menu-button}',
  1024. description: 'Template for the row contents.\n\nElements inside of p:{ &nbsp; } are hidden if no sound is playing.\nElements inside of h:{ &nbsp; } are hidden and appear on hover.',
  1025. showInSettings: 'textarea'
  1026. },
  1027. {
  1028. property: 'footerTemplate',
  1029. title: 'Footer Contents',
  1030. actions: [{
  1031. title: 'Reset',
  1032. handler: 'settings.reset'
  1033. }],
  1034. default:
  1035. '<div class="fc-sounds-footer-left">\n' +
  1036. ' playing-button:"sound-index /"&nbsp;sound-count ui-files-icon\n' +
  1037. '</div>\n\n' +
  1038. '<div class="fc-sounds-footer-right">\n' +
  1039. ' sound-tag-toggle-button\n' +
  1040. ' p:{\n' +
  1041. ' post-link\n' +
  1042. ' \n' +
  1043. ' <span class="fc-sounds-footer-text">&nbsp;Open:</span>\n' +
  1044. ' ui-bracketL-icon\n' +
  1045. ' image-link sound-link\n' +
  1046. ' ui-bracketR-icon\n' +
  1047. ' \n' +
  1048. ' <span class="fc-sounds-footer-text">Download:</span>\n' +
  1049. ' ui-bracketL-icon\n' +
  1050. ' dl-image-button dl-sound-button\n' +
  1051. ' ui-bracketR-icon\n' +
  1052. ' }\n' +
  1053. '</div>\n',
  1054. description: 'Template for the footer contents.\n\nElements inside of p:{ &nbsp; } are hidden if no sound is playing.\nElements inside of h:{ &nbsp; } are hidden and appear on hover.',
  1055. showInSettings: 'textarea',
  1056. attrs: 'style="height:120px;"'
  1057. },
  1058. {
  1059. property: 'chanXTemplate',
  1060. title: '4chan X Header Controls',
  1061. default: 'p:{\n\tpost-link:"sound-name"\n\tprev-button\n\tplay-button\n\tnext-button\n\tsound-current-time / sound-duration\n}',
  1062. actions: [{
  1063. title: 'Reset',
  1064. handler: 'settings.reset'
  1065. }],
  1066. /*showInSettings: 'textarea'*/
  1067. showInSettings: false,
  1068. },
  1069. {
  1070. title: 'Colors',
  1071. showInSettings: true,
  1072. property: 'colors',
  1073. updateStylesheet: true,
  1074. actions: [{
  1075. title: 'Match Theme',
  1076. handler: 'settings.forceBoardTheme'
  1077. }],
  1078. // These colors will be overriden with the theme defaults at initialization.
  1079. settings: [{
  1080. property: 'colors.text',
  1081. default: 'rgba(0, 0, 0, 1)',
  1082. title: 'Text'
  1083. },
  1084. {
  1085. property: 'colors.background',
  1086. default: 'rgba(214, 218, 240, 1)',
  1087. title: 'Background'
  1088. },
  1089. {
  1090. property: 'colors.border',
  1091. default: 'rgba(183, 197, 217, 1)',
  1092. title: 'Border'
  1093. },
  1094. {
  1095. property: 'colors.odd_row',
  1096. default: 'rgba(214, 218, 240, 1)',
  1097. title: 'Odd Row',
  1098. },
  1099. {
  1100. property: 'colors.even_row',
  1101. default: 'rgba(183, 197, 217, 1)',
  1102. title: 'Even Row'
  1103. },
  1104. {
  1105. property: 'colors.playing',
  1106. default: 'rgba(152, 191, 247, 1)',
  1107. title: 'Playing Row'
  1108. },
  1109. {
  1110. property: 'colors.dragging',
  1111. default: 'rgba(195, 150, 200, 1)',
  1112. title: 'Dragging Row'
  1113. },
  1114. {
  1115. property: 'colors.text_playing',
  1116. default: 'rgba(0, 0, 0, 1)',
  1117. title: '<span style="font-size: 13.5px; margin: 0.2em 0;">Text color of the<br>playing/dragging row</span>'
  1118. },
  1119. {
  1120. property: 'colors.controls_panel',
  1121. default: 'rgba(63, 63, 68, 1)',
  1122. title: '<span style="font-size: 13.5px; margin: 0.2em 0;">Playback Controls<br>Panel Background</span>',
  1123. },
  1124. {
  1125. property: 'colors.buttons_color',
  1126. default: 'rgba(255, 255, 255, 1)',
  1127. title: 'Buttons'
  1128. },
  1129. {
  1130. property: 'colors.hover_color',
  1131. default: 'rgba(0, 182, 240, 1)',
  1132. title: 'Hover',
  1133. },
  1134. {
  1135. property: 'colors.controls_current_time',
  1136. default: 'rgba(255, 255, 255, 1)',
  1137. title: 'Current Time'
  1138. },
  1139. {
  1140. property: 'colors.controls_duration',
  1141. default: 'rgba(144, 144, 144, 1)',
  1142. title: 'Duration'
  1143. },
  1144. {
  1145. property: 'colors.progress_bar',
  1146. default: 'rgba(140, 140, 140, 1)',
  1147. title: '<span style="margin: 0.2em 0;">Progress Bar<br>Background</span>',
  1148. },
  1149. {
  1150. property: 'colors.progress_bar_loaded',
  1151. default: 'rgba(90, 90, 91, 1)',
  1152. title: '<span style="margin: 0.25em 0 0.2em 0;">Loaded Bar<br>Background</span>',
  1153. }
  1154. ]
  1155. },
  1156.  
  1157. ];
  1158.  
  1159.  
  1160. }),
  1161. /* 2 - Core Player Setup
  1162. • Initializes the main Player object with:
  1163. o Component references (controls, playlist, etc.)
  1164. o Template system
  1165. o Event system
  1166. • Key functions:
  1167. o initialize(): Bootstraps all components
  1168. o compareIds(): For sorting sounds
  1169. o acceptedSound(): Validates URLs against allowlist
  1170. o syncTab(): Handles cross-tab synchronization
  1171. */
  1172. (function(module, exports, __webpack_require__) {
  1173.  
  1174. const components = {
  1175. // Settings must be first.
  1176. settings: __webpack_require__(5),
  1177. controls: __webpack_require__(6),
  1178. display: __webpack_require__(7),
  1179. events: __webpack_require__(8),
  1180. footer: __webpack_require__(9),
  1181. header: __webpack_require__(10),
  1182. hotkeys: __webpack_require__(11),
  1183. minimised: __webpack_require__(12),
  1184. playlist: __webpack_require__(13),
  1185. position: __webpack_require__(14),
  1186. threads: __webpack_require__(15),
  1187. userTemplate: __webpack_require__(17)
  1188. };
  1189.  
  1190. // Create a global ref to the player.
  1191. const Player = window.Player = module.exports = {
  1192. //ns,
  1193. audio: new Audio(),
  1194. sounds: [],
  1195. isHidden: true,
  1196. container: null,
  1197. ui: {},
  1198.  
  1199. // Build the config from the default
  1200. config: {},
  1201.  
  1202. // Helper function to query elements in the player.
  1203. $: (...args) => Player.container && Player.container.querySelector(...args),
  1204. $all: (...args) => Player.container && Player.container.querySelectorAll(...args),
  1205.  
  1206. // Store a ref to the components so they can be iterated.
  1207. components,
  1208.  
  1209. // Get all the templates.
  1210. templates: {
  1211. body: __webpack_require__(19),
  1212. controls: __webpack_require__(20),
  1213. css: __webpack_require__(21),
  1214. footer: __webpack_require__(22),
  1215. header: __webpack_require__(23),
  1216. itemMenu: __webpack_require__(24),
  1217. list: __webpack_require__(25),
  1218. player: __webpack_require__(26),
  1219. settings: __webpack_require__(27),
  1220. threads: __webpack_require__(28),
  1221. threadBoards: __webpack_require__(29),
  1222. threadList: __webpack_require__(30),
  1223. galleryList: __webpack_require__(31)
  1224. },
  1225.  
  1226. /**
  1227. * Set up the player.
  1228. */
  1229. initialize: async function initialize() {
  1230. if (Player.initialized) {
  1231. return;
  1232. }
  1233. Player.initialized = true;
  1234. try {
  1235. Player.sounds = [];
  1236. // Run the initialisation for each component.
  1237. for (let name in components) {
  1238. components[name].initialize && await components[name].initialize();
  1239. }
  1240.  
  1241. if (!is4chan) {
  1242. // Add a sounds link in the nav for archives
  1243. const nav = document.querySelector('.navbar-inner .nav:nth-child(2)');
  1244. const li = createElement('<li><a href="javascript:;">Sounds</a></li>', nav);
  1245. li.children[0].addEventListener('click', Player.display.toggle);
  1246. } else if (isChanX) {
  1247. // If it's already known that 4chan X is running then setup the button for it.
  1248. Player.display.initChanX();
  1249. } else {
  1250. // Add the [Sounds] link in the top and bottom nav.
  1251. document.querySelectorAll('#settingsWindowLink, #settingsWindowLinkBot').forEach(function(link) {
  1252. const showLink = createElement('<a href="javascript:;">Sounds</a>', null, {
  1253. click: Player.display.toggle
  1254. });
  1255. link.parentNode.insertBefore(showLink, link);
  1256. link.parentNode.insertBefore(document.createTextNode('] ['), link);
  1257. });
  1258. }
  1259.  
  1260. // Render the player, but not neccessarily show it.
  1261. Player.display.render();
  1262. // show the player
  1263. Player.display.show();
  1264. } catch (err) {
  1265. Player.logError('There was an error initialzing the sound player. Please check the console for details.');
  1266. console.error('[8chan sounds player]', err);
  1267. // Can't recover so throw this error.
  1268. throw err;
  1269. }
  1270. },
  1271.  
  1272. /**
  1273. * Compare two ids for sorting.
  1274. */
  1275. compareIds: function(a, b) {
  1276. const [aPID, aSID] = a.split(':');
  1277. const [bPID, bSID] = b.split(':');
  1278. const postDiff = aPID - bPID;
  1279. return postDiff !== 0 ? postDiff : aSID - bSID;
  1280. },
  1281.  
  1282. /**
  1283. * Check whether a sound src and image are allowed and not filtered.
  1284. */
  1285. acceptedSound: function({
  1286. src,
  1287. imageMD5
  1288. }) {
  1289. try {
  1290. const link = new URL(src);
  1291. const host = link.hostname.toLowerCase();
  1292. return !Player.config.filters.find(v => v === imageMD5 || v === host + link.pathname) &&
  1293. Player.config.allow.find(h => host === h || host.endsWith('.' + h));
  1294. } catch (err) {
  1295. return false;
  1296. }
  1297. },
  1298.  
  1299. /**
  1300. * Listen for changes
  1301. */
  1302. syncTab: (property, callback) => GM_addValueChangeListener(property, (_prop, oldValue, newValue, remote) => {
  1303. remote && callback(newValue, oldValue);
  1304. }),
  1305.  
  1306. /**
  1307. * Send an error notification event.
  1308. */
  1309. logError: function(message, type = 'error') {
  1310. console.error(message);
  1311. document.dispatchEvent(new CustomEvent('CreateNotification', {
  1312. bubbles: true,
  1313. detail: {
  1314. type: type,
  1315. content: message,
  1316. lifetime: 5
  1317. }
  1318. }));
  1319. }
  1320. };
  1321.  
  1322. // Add each of the components to the player.
  1323. for (let name in components) {
  1324. Player[name] = components[name];
  1325. (Player[name].atRoot || []).forEach(k => Player[k] = Player[name][k]);
  1326. }
  1327.  
  1328.  
  1329. }),
  1330. /* 3 - Main Entry Point
  1331. • Initialization sequence:
  1332. a. Waits for DOM/4chan X readiness
  1333. b. Sets up mutation observer for dynamic content
  1334. c. Triggers initial page scan
  1335. • Handles both:
  1336. o Native 4chan interface
  1337. o 4chan X extension environment
  1338. */
  1339. (function(module, __webpack_exports__, __webpack_require__) {
  1340. "use strict";
  1341. __webpack_require__.r(__webpack_exports__);
  1342. const _globals__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
  1343. const _player__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
  1344. const _file_parser__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(0);
  1345.  
  1346. async function doInit() {
  1347. setTimeout(async function() {
  1348. await _player__WEBPACK_IMPORTED_MODULE_1__.initialize();
  1349. Player.set('showSoundTagOnly', false);
  1350.  
  1351. // Initialize header and footer buttons
  1352. _player__WEBPACK_IMPORTED_MODULE_1__.display.initHeader();
  1353. _player__WEBPACK_IMPORTED_MODULE_1__.display.initFooter();
  1354.  
  1355. // Parse existing posts
  1356. _file_parser__WEBPACK_IMPORTED_MODULE_2__.parseFiles(document.body, true);
  1357.  
  1358. // Add sounds link to 8chan navigation
  1359. const nav = document.querySelector('.threadBottom .innerUtility');
  1360. if (nav && !document.querySelector('.innerUtility a[href="javascript:;"]')) {
  1361. const li = createElement('<a href="javascript:;">Sounds</a>', nav);
  1362. nav.insertBefore(document.createTextNode(' ['), li);
  1363. nav.insertBefore(li, nav.querySelector('.archiveLinkThread'));
  1364. nav.insertBefore(document.createTextNode('] '), nav.querySelector('.archiveLinkThread'));
  1365. li.addEventListener('click', _player__WEBPACK_IMPORTED_MODULE_1__.display.toggle);
  1366. }
  1367.  
  1368. _file_parser__WEBPACK_IMPORTED_MODULE_2__.parseFiles(document.body, true);
  1369.  
  1370. // Set up mutation observer
  1371. const observer = new MutationObserver(function(mutations) {
  1372. mutations.forEach(function(mutation) {
  1373. if (mutation.type === 'childList') {
  1374. mutation.addedNodes.forEach(function(node) {
  1375. if (node.nodeType === Node.ELEMENT_NODE) {
  1376. _file_parser__WEBPACK_IMPORTED_MODULE_2__.parseFiles(node);
  1377. }
  1378. });
  1379. }
  1380. });
  1381. });
  1382.  
  1383. observer.observe(document.body, {
  1384. childList: true,
  1385. subtree: true
  1386. });
  1387. }, 0);
  1388. }
  1389.  
  1390. document.addEventListener('DOMContentLoaded', doInit);
  1391. }),
  1392. /* 4 - Globals & Utilities
  1393. • Defines shared utilities:
  1394. o _set()/_get(): Deep object property access
  1395. o toDuration(): Formats time (00:00)
  1396. o timeAgo(): Relative time formatting
  1397. o createElement(): DOM creation helper
  1398. o noDefault(): Event handler wrapper
  1399. • Sets global constants:
  1400. o ns: Namespace prefix
  1401. o is4chan/isChanX: Environment detection
  1402. o Board: Current board name
  1403. o VERSION
  1404. • Load in glyphs
  1405. */
  1406. (function(module, exports, __webpack_require__) {
  1407. // Update globals for 8chan
  1408. window.ns = 'fc-sounds';
  1409. window.is4chan = false;
  1410. window.isChanX = false;
  1411. window.Board = location.pathname.split('/')[1];
  1412. window.localFileCounter = 0;
  1413. window.isLoading = false;
  1414. window.Master = undefined;
  1415. window.Slave = undefined;
  1416. window.mediaStatus = undefined;
  1417.  
  1418. const scriptVersion = GM_info.script.version;
  1419. window.VERSION = scriptVersion ? scriptVersion : 'Version not found';
  1420.  
  1421. // Keep rest of original globals.js content
  1422. window._set = function(object, path, value) {
  1423. const props = path.split('.');
  1424. const lastProp = props.pop();
  1425. const setOn = props.reduce((obj, k) => obj[k] || (obj[k] = {}), object);
  1426. setOn && (setOn[lastProp] = value);
  1427. return object;
  1428. };
  1429.  
  1430. window._get = function(object, path, dflt) {
  1431. const props = path.split('.');
  1432. const lastProp = props.pop();
  1433. const parent = props.reduce((obj, k) => obj && obj[k], object);
  1434. return parent && Object.prototype.hasOwnProperty.call(parent, lastProp) ?
  1435. parent[lastProp] :
  1436. dflt;
  1437. };
  1438.  
  1439. window.toDuration = function(number) {
  1440. number = Math.floor(number || 0);
  1441. let [seconds, minutes, hours] = _duration(0, number);
  1442. seconds < 10 && (seconds = '0' + seconds);
  1443. return (hours ? hours + ':' : '') + minutes + ':' + seconds;
  1444. };
  1445.  
  1446. window.timeAgo = function(date) {
  1447. const [seconds, minutes, hours, days, weeks] = _duration(Math.floor(date), Math.floor(Date.now() / 1000));
  1448. /* _eslint-disable indent */
  1449. return weeks > 1 ? weeks + ' weeks ago' :
  1450. days > 0 ? days + (days === 1 ? ' day' : ' days') + ' ago' :
  1451. hours > 0 ? hours + (hours === 1 ? ' hour' : ' hours') + ' ago' :
  1452. minutes > 0 ? minutes + (minutes === 1 ? ' minute' : ' minutes') + ' ago' :
  1453. seconds + (seconds === 1 ? ' second' : ' seconds') + ' ago';
  1454. /* eslint-enable indent */
  1455. };
  1456.  
  1457. function _duration(from, to) {
  1458. const diff = Math.max(0, to - from);
  1459. return [
  1460. diff % 60,
  1461. Math.floor(diff / 60) % 60,
  1462. Math.floor(diff / 60 / 60) % 24,
  1463. Math.floor(diff / 60 / 60 / 24) % 7,
  1464. Math.floor(diff / 60 / 60 / 24 / 7)
  1465. ];
  1466. }
  1467.  
  1468. window.createElement = function(html, parent, events = {}) {
  1469. const container = document.createElement('div');
  1470. container.innerHTML = html;
  1471. const el = container.children[0];
  1472. parent && parent.appendChild(el);
  1473. for (let event in events) {
  1474. el.addEventListener(event, events[event]);
  1475. }
  1476. return el;
  1477. };
  1478.  
  1479. window.createElementBefore = function(html, before, events = {}) {
  1480. const el = createElement(html, null, events);
  1481. before.parentNode.insertBefore(el, before);
  1482. return el;
  1483. };
  1484.  
  1485. window.noDefault = (f, ...args) => e => {
  1486. e.preventDefault();
  1487. const func = typeof f === 'function' ? f : _get(Player, f);
  1488. func(...args);
  1489. };
  1490.  
  1491. window.throttleFc = function(func, limit) {
  1492. let inThrottle;
  1493. return function() {
  1494. const args = arguments;
  1495. const context = this;
  1496. if (!inThrottle) {
  1497. func.apply(context, args);
  1498. inThrottle = true;
  1499. setTimeout(() => inThrottle = false, limit);
  1500. }
  1501. }
  1502. };
  1503.  
  1504. window.debounceFc = function(func, timeout = 300){
  1505. let timer;
  1506. return (...args) => {
  1507. clearTimeout(timer);
  1508. timer = setTimeout(() => { func.apply(this, args); }, timeout);
  1509. };
  1510. };
  1511. }),
  1512. /* 5 - Settings Manager
  1513. • Manages all user configuration:
  1514. o load()/save(): Persistent storage
  1515. o set(): Updates settings with validation
  1516. o applyBoardTheme(): Matches 8chan's colors
  1517. • Handles:
  1518. o Settings UI rendering
  1519. o Change detection
  1520. o Cross-tab synchronization
  1521. */
  1522. (function(module, exports, __webpack_require__) {
  1523.  
  1524. const settingsConfig = __webpack_require__(1);
  1525.  
  1526. module.exports = {
  1527. atRoot: ['set'],
  1528.  
  1529. delegatedEvents: {
  1530. click: {
  1531. [`.${ns}-settings .${ns}-heading-action`]: 'settings.handleAction',
  1532. },
  1533. focusout: {
  1534. [`.${ns}-settings input, .${ns}-settings textarea`]: 'settings.handleChange'
  1535. },
  1536. change: {
  1537. [`.${ns}-settings input[type=checkbox], .${ns}-settings select`]: 'settings.handleChange'
  1538. },
  1539. keydown: {
  1540. [`.${ns}-key-input`]: 'settings.handleKeyChange',
  1541. },
  1542. keyup: {
  1543. [`.${ns}-encoded-input`]: 'settings._handleEncoded',
  1544. [`.${ns}-decoded-input`]: 'settings._handleDecoded'
  1545. }
  1546. },
  1547.  
  1548. initialize: async function() {
  1549. await Player.settings.updateLegacySettings();
  1550.  
  1551. // Apply the default config.
  1552. Player.config = settingsConfig.reduce(function reduceSettings(config, setting) {
  1553. if (setting.settings) {
  1554. setting.settings.forEach(subSetting => {
  1555. let _setting = {
  1556. ...setting,
  1557. ...subSetting
  1558. };
  1559. _set(config, _setting.property, _setting.default);
  1560. });
  1561. return config;
  1562. }
  1563. return _set(config, setting.property, setting.default);
  1564. }, {});
  1565.  
  1566. // Load the user config.
  1567. await Player.settings.load();
  1568.  
  1569. // Apply the default board theme as default.
  1570. Player.settings.applyBoardTheme();
  1571.  
  1572. // Listen for the player closing to apply the pause on hide setting.
  1573. Player.on('hide', function() {
  1574. if (Player.config.pauseOnHide) {
  1575. Player.pause();
  1576. }
  1577. });
  1578.  
  1579. // Listen for changes from other tabs
  1580. Player.syncTab('settings', value => Player.settings.apply(value, {
  1581. bypassSave: true,
  1582. applyDefault: true,
  1583. ignore: ['viewStyle']
  1584. }));
  1585.  
  1586. // Apply the default board theme as default again just in case the script loaded before the CSS
  1587. setTimeout(() => {Player.settings.applyBoardTheme()}, 1000);
  1588. },
  1589.  
  1590. updateLegacySettings: async function() {
  1591. try {
  1592. const savedSettings = await GM.getValue('settings');
  1593.  
  1594. // If no settings exist, initialize with default values
  1595. if (!savedSettings) {
  1596. const defaultSettings = {
  1597. viewStyle: "gallery",
  1598. VERSION: GM_info.script.version,
  1599. };
  1600. await GM.setValue('settings', JSON.stringify(defaultSettings));
  1601. return;
  1602. }
  1603.  
  1604. // Parse settings safely (handle malformed JSON)
  1605. let settings;
  1606. try {
  1607. settings = JSON.parse(savedSettings);
  1608. } catch (e) {
  1609. console.error("[8chan sounds player] Failed to parse settings, resetting to defaults.", e);
  1610. settings = {
  1611. viewStyle: "gallery",
  1612. VERSION: GM_info.script.version,
  1613. };
  1614. await GM.setValue('settings', JSON.stringify(settings));
  1615. return;
  1616. }
  1617.  
  1618. // Ensure VERSION exists and is in the correct format
  1619. if (!settings.VERSION) {
  1620. settings.viewStyle = "gallery";
  1621. settings.VERSION = GM_info.script.version;
  1622. await GM.setValue('settings', JSON.stringify(settings));
  1623. return;
  1624. }
  1625.  
  1626. // Safely split and compare version (handle unexpected version formats)
  1627. const versionParts = settings.VERSION.split(/_/, 2);
  1628. if (versionParts.length < 2 || isNaN(versionParts[1]) || parseInt(versionParts[1]) < 33) {
  1629. settings.viewStyle = "gallery";
  1630. settings.VERSION = GM_info.script.version;
  1631. await GM.setValue('settings', JSON.stringify(settings));
  1632. }
  1633. } catch (error) {
  1634. console.error("[8chan sounds player] Failed to update settings:", error);
  1635. }
  1636. },
  1637.  
  1638. render: function() {
  1639. if (Player.container) {
  1640. Player.$(`.${ns}-settings`).innerHTML = Player.templates.settings();
  1641. }
  1642. },
  1643.  
  1644. forceBorderWidth: function() {
  1645. Player.settings.applyBorderWidth(true);
  1646. Player.settings.save();
  1647. },
  1648.  
  1649. applyBorderWidth: function(force) {
  1650. const innerPostElement = document.querySelector('.divPosts .postCell:not(.postCell:target) .innerPost');
  1651. const innerPostStyle = (!innerPostElement) ? null : window.getComputedStyle(innerPostElement);
  1652.  
  1653. let borderWidth = (innerPostStyle !== null) ? innerPostStyle.getPropertyValue('border-right-width') : '1px';
  1654. borderWidth = Math.max(0.1, Math.min(2, /*Math.round(*/parseFloat(borderWidth)/*)*/)) + 'px' || '1px';
  1655.  
  1656. Player.set('borderWidth', borderWidth, { bypassSave: true, bypassRender: true });
  1657.  
  1658. // Updated the stylesheet if it exists.
  1659. Player.stylesheet && Player.display.updateStylesheet();
  1660. // Re-render the settings if needed.
  1661. Player.settings.render();
  1662. },
  1663.  
  1664. forceBoardTheme: function() {
  1665. Player.settings.applyBoardTheme(true);
  1666. Player.settings.save();
  1667. },
  1668.  
  1669. applyBoardTheme: function(force) {
  1670. const rootStyles = window.getComputedStyle(document.documentElement);
  1671. //console.log(rootStyles);
  1672. const linkElement = document.querySelector('.panelBacklinks a');
  1673. const innerPostElement = document.querySelector('.divPosts .postCell:not(.postCell:target) .innerPost');
  1674. const linkStyle = (!linkElement) ? null : window.getComputedStyle(linkElement);
  1675. const innerPostStyle = (!innerPostElement) ? null : window.getComputedStyle(innerPostElement);
  1676. const selectedTheme = localStorage.getItem('selectedTheme');
  1677.  
  1678. let textColor = rootStyles.getPropertyValue('--text-color').trim() || 'rgba(0,0,0,1)';
  1679. let linkColor = (linkStyle !== null) ? linkStyle.getPropertyValue('color') : rootStyles.getPropertyValue('--link-color').trim() || 'rgba(152,191,247,1)';
  1680. let backgroundColor = (innerPostStyle !== null) ? innerPostStyle.getPropertyValue('background-color') : rootStyles.getPropertyValue('--contrast-color').trim() || rootStyles.getPropertyValue('--background-color').trim() || 'rgba(255,255,255,1)';
  1681. let borderColor = (innerPostStyle !== null) ? innerPostStyle.getPropertyValue('border-bottom-color') : rootStyles.getPropertyValue('--horizon-sep-color').trim() || rootStyles.getPropertyValue('--border-color').trim() || 'rgba(183,197,217,1)';
  1682.  
  1683. let linkHoverColor = rootStyles.getPropertyValue('--link-hover-color').trim() || 'rgba(53,133,244,1)';
  1684. let windowsColor = rootStyles.getPropertyValue('--windows-focused-background').trim() || null;
  1685.  
  1686. textColor = Player.settings.isLightColor(backgroundColor) ? Player.settings.adjustColor(textColor, { h: 0, s: -3, v: -3, a:1 }) : Player.settings.adjustColor(textColor, { h: 0, s: -3, v: 3, a:1 });
  1687. linkColor = Player.settings.adjustColor(linkColor, { h: 0, s: 0, v: 0, a:1 });
  1688. backgroundColor = Player.settings.adjustColor(backgroundColor, { h: 0, s: 0, v: 0, a:1 });
  1689. borderColor = Player.settings.adjustColor(borderColor, { h: 0, s: 0, v: 0, a:1 });
  1690. linkHoverColor = Player.settings.adjustColor(linkHoverColor, { h: 0, s: 0, v: 0, a:1 });
  1691.  
  1692. borderColor = (borderColor === backgroundColor) ? Player.settings.mixColors(borderColor, textColor, 0.3) : borderColor;
  1693.  
  1694. const oddRow = backgroundColor;
  1695. const evenRow = Player.settings.mixColors(textColor, oddRow, 0.94);
  1696.  
  1697. const controlsPanel = Player.settings.mixColors(backgroundColor, textColor, 0.11);
  1698. const buttonsColor = (windowsColor !== null) ? Player.settings.adjustColor(windowsColor, { h: 0, s: 0, v: 0, a:1 }) : Player.settings.mixColors(textColor, linkColor, 0.85);
  1699. const hoverColor = linkHoverColor;
  1700. const controlsCurrentTime = textColor;
  1701. const controlsDuration = Player.settings.mixColors(controlsPanel, textColor, 0.6);
  1702.  
  1703. let textPlaying;
  1704. switch (selectedTheme) {
  1705. case "evita":
  1706. textPlaying = textColor;
  1707. break;
  1708. case "vivian":
  1709. textPlaying = 'rgba(208,208,208,1.0)';
  1710. break;
  1711. case "warosu":
  1712. textPlaying = 'rgba(245,245,245,1.0)';
  1713. break;
  1714. default:
  1715. textPlaying = Player.settings.isLightColor(backgroundColor)
  1716. ? (Player.settings.isLightColor(textColor) ? 'rgba(22,22,22,1.0)' : backgroundColor)
  1717. : (Player.settings.isLightColor(textColor) ? 'rgba(218,218,218,1.0)' : backgroundColor);
  1718. }
  1719.  
  1720.  
  1721.  
  1722. const playing = Player.settings.isLightColor(backgroundColor)
  1723. ? Player.settings.adjustColor(buttonsColor, { h: 0, s: 0, v: 0, a:0.62 })
  1724. : Player.settings.adjustColor(buttonsColor, { h: 0, s: 0, v: 0, a:0.42 });
  1725. let dragging = Player.settings.mixColors(backgroundColor, buttonsColor, 0.8);
  1726. dragging = Player.settings.adjustColor(dragging, { h: 0, s: 0, v: 0, a:0.7 });
  1727.  
  1728. let progressBarLoaded = Player.settings.mixColors(backgroundColor, buttonsColor, 0.35);
  1729. progressBarLoaded = Player.settings.mixColors(progressBarLoaded, linkColor, 0.05);;
  1730. const progressBar = Player.settings.isLightColor(controlsPanel) ? Player.settings.adjustColor(progressBarLoaded, { h: 0, s: -10, v: -5, a:0.7 }) : Player.settings.adjustColor(progressBarLoaded, { h: 0, s: -10, v: 5, a:0.7 });
  1731.  
  1732. const colorSettingMap = {
  1733. 'colors.text': textColor,
  1734. 'colors.background': backgroundColor,
  1735. 'colors.border': borderColor,
  1736. 'colors.odd_row': oddRow,
  1737. 'colors.even_row': evenRow,
  1738. 'colors.playing': playing,
  1739. 'colors.dragging': dragging,
  1740. 'colors.text_playing': textPlaying,
  1741.  
  1742. 'colors.controls_panel': controlsPanel,
  1743. 'colors.buttons_color': buttonsColor,
  1744. 'colors.hover_color': hoverColor,
  1745. 'colors.controls_current_time': controlsCurrentTime,
  1746. 'colors.controls_duration': controlsDuration,
  1747. 'colors.progress_bar': progressBar,
  1748. 'colors.progress_bar_loaded': progressBarLoaded,
  1749. };
  1750.  
  1751. settingsConfig.find(s => s.property === 'colors').settings.forEach(setting => {
  1752. const updateConfig = force || (setting.default === _get(Player.config, setting.property));
  1753. colorSettingMap[setting.property] && (setting.default = colorSettingMap[setting.property]);
  1754. updateConfig && Player.set(setting.property, setting.default, {
  1755. bypassSave: true,
  1756. bypassRender: true
  1757. });
  1758. });
  1759.  
  1760. // Updated the stylesheet if it exists.
  1761. Player.stylesheet && Player.display.updateStylesheet();
  1762.  
  1763. // Re-render the settings if needed.
  1764. Player.settings.render();
  1765.  
  1766. Player.settings.applyBorderWidth();
  1767. },
  1768.  
  1769. parseColor: function(color) {
  1770. let result;
  1771.  
  1772. // Named HTML colors to hex mapping
  1773. const htmlColors = {"aliceblue":"#f0f8ff","antiquewhite":"#faebd7","aqua":"#00ffff","aquamarine":"#7fffd4","azure":"#f0ffff","beige":"#f5f5dc","bisque":"#ffe4c4","black":"#000000","blanchedalmond":"#ffebcd","blue":"#0000ff","blueviolet":"#8a2be2","brown":"#a52a2a","burlywood":"#deb887","cadetblue":"#5f9ea0","chartreuse":"#7fff00","chocolate":"#d2691e","coral":"#ff7f50","cornflowerblue":"#6495ed","cornsilk":"#fff8dc","crimson":"#dc143c","cyan":"#00ffff","darkblue":"#00008b","darkcyan":"#008b8b","darkgoldenrod":"#b8860b","darkgray":"#a9a9a9","darkgreen":"#006400","darkkhaki":"#bdb76b","darkmagenta":"#8b008b","darkolivegreen":"#556b2f","darkorange":"#ff8c00","darkorchid":"#9932cc","darkred":"#8b0000","darksalmon":"#e9967a","darkseagreen":"#8fbc8f","darkslateblue":"#483d8b","darkslategray":"#2f4f4f","darkturquoise":"#00ced1","darkviolet":"#9400d3","deeppink":"#ff1493","deepskyblue":"#00bfff","dimgray":"#696969","dodgerblue":"#1e90ff","firebrick":"#b22222","floralwhite":"#fffaf0","forestgreen":"#228b22","fuchsia":"#ff00ff","gainsboro":"#dcdcdc","ghostwhite":"#f8f8ff","gold":"#ffd700","goldenrod":"#daa520","gray":"#808080","green":"#008000","greenyellow":"#adff2f","honeydew":"#f0fff0","hotpink":"#ff69b4","indianred ":"#cd5c5c","indigo":"#4b0082","ivory":"#fffff0","khaki":"#f0e68c","lavender":"#e6e6fa","lavenderblush":"#fff0f5","lawngreen":"#7cfc00","lemonchiffon":"#fffacd","lightblue":"#add8e6","lightcoral":"#f08080","lightcyan":"#e0ffff","lightgoldenrodyellow":"#fafad2","lightgrey":"#d3d3d3","lightgreen":"#90ee90","lightpink":"#ffb6c1","lightsalmon":"#ffa07a","lightseagreen":"#20b2aa","lightskyblue":"#87cefa","lightslategray":"#778899","lightsteelblue":"#b0c4de","lightyellow":"#ffffe0","lime":"#00ff00","limegreen":"#32cd32","linen":"#faf0e6","magenta":"#ff00ff","maroon":"#800000","mediumaquamarine":"#66cdaa","mediumblue":"#0000cd","mediumorchid":"#ba55d3","mediumpurple":"#9370d8","mediumseagreen":"#3cb371","mediumslateblue":"#7b68ee","mediumspringgreen":"#00fa9a","mediumturquoise":"#48d1cc","mediumvioletred":"#c71585","midnightblue":"#191970","mintcream":"#f5fffa","mistyrose":"#ffe4e1","moccasin":"#ffe4b5","navajowhite":"#ffdead","navy":"#000080","oldlace":"#fdf5e6","olive":"#808000","olivedrab":"#6b8e23","orange":"#ffa500","orangered":"#ff4500","orchid":"#da70d6","palegoldenrod":"#eee8aa","palegreen":"#98fb98","paleturquoise":"#afeeee","palevioletred":"#d87093","papayawhip":"#ffefd5","peachpuff":"#ffdab9","peru":"#cd853f","pink":"#ffc0cb","plum":"#dda0dd","powderblue":"#b0e0e6","purple":"#800080","rebeccapurple":"#663399","red":"#ff0000","rosybrown":"#bc8f8f","royalblue":"#4169e1","saddlebrown":"#8b4513","salmon":"#fa8072","sandybrown":"#f4a460","seagreen":"#2e8b57","seashell":"#fff5ee","sienna":"#a0522d","silver":"#c0c0c0","skyblue":"#87ceeb","slateblue":"#6a5acd","slategray":"#708090","snow":"#fffafa","springgreen":"#00ff7f","steelblue":"#4682b4","tan":"#d2b48c","teal":"#008080","thistle":"#d8bfd8","tomato":"#ff6347","turquoise":"#40e0d0","violet":"#ee82ee","wheat":"#f5deb3","white":"#ffffff","whitesmoke":"#f5f5f5","yellow":"#ffff00","yellowgreen":"#9acd32"};
  1774.  
  1775. // Convert named color to hex first if it exists
  1776. if (htmlColors[color.toLowerCase()]) {
  1777. color = htmlColors[color.toLowerCase()];
  1778. }
  1779.  
  1780. // Helper function to validate and clamp RGB values
  1781. const clampRGB = (value) => Math.min(255, Math.max(0, parseInt(value, 10)));
  1782.  
  1783. // Helper function to validate and clamp alpha values
  1784. const clampAlpha = (value) => {
  1785. const num = parseFloat(value);
  1786. return Math.min(1, Math.max(0, isNaN(num) ? 1 : num));
  1787. };
  1788.  
  1789. // Hex formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
  1790. if (/^#([0-9A-Fa-f]{3,4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(color)) {
  1791. let hex = color.slice(1);
  1792. // Expand shorthand (e.g., #RGBA → #RRGGBBAA)
  1793. if (hex.length === 3 || hex.length === 4) {
  1794. hex = hex.split('').map(x => x + x).join('');
  1795. }
  1796. // Parse to [r, g, b, a] (alpha defaults to 1 if missing)
  1797. const r = clampRGB(parseInt(hex.slice(0, 2), 16));
  1798. const g = clampRGB(parseInt(hex.slice(2, 4), 16));
  1799. const b = clampRGB(parseInt(hex.slice(4, 6), 16));
  1800. const a = hex.length === 8 ? clampAlpha(parseInt(hex.slice(6, 8), 16) / 255) : 1;
  1801. return [r, g, b, a];
  1802. }
  1803. // RGB: rgb(r, g, b) → [r, g, b, 1]
  1804. else if (/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(color)) {
  1805. const matches = color.match(/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i);
  1806. const r = clampRGB(matches[1]);
  1807. const g = clampRGB(matches[2]);
  1808. const b = clampRGB(matches[3]);
  1809. return [r, g, b, 1];
  1810. }
  1811. // RGBA: rgba(r, g, b, a) → [r, g, b, a]
  1812. else if (/^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([01]?\.\d+|0|1)\s*\)$/i.test(color)) {
  1813. const matches = color.match(/rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([01]?\.\d+|0|1)\s*\)/i);
  1814. const r = clampRGB(matches[1]);
  1815. const g = clampRGB(matches[2]);
  1816. const b = clampRGB(matches[3]);
  1817. const a = clampAlpha(matches[4]);
  1818. return [r, g, b, a];
  1819. }
  1820. // Return null if format is invalid
  1821. return null;
  1822. },
  1823.  
  1824. isLightColor: function(color) {
  1825. const rgba = Player.settings.parseColor(color);
  1826. if (!rgba) return false;
  1827.  
  1828. // Extract RGB components (ignore alpha for luminance calculation)
  1829. const [r, g, b] = rgba;
  1830.  
  1831. // Calculate luminance
  1832. const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
  1833.  
  1834. // Return true if luminance exceeds threshold (102)
  1835. return luminance > 102;
  1836. },
  1837.  
  1838. /**
  1839. * Checks if a color's hue is above (greater than) yellow (60°).
  1840. * @param {string} color - Input color (hex, rgb, rgba, or named color)
  1841. * @returns {boolean|null} - Returns:
  1842. * - `true` if hue > 60° (e.g., greens, blues, purples)
  1843. * - `false` if hue ≤ 60° (e.g., reds, oranges, yellows)
  1844. * - `null` if color is invalid or grayscale (no hue)
  1845. */
  1846. isHueAboveYellow: function(color) {
  1847. const rgba = Player.settings.parseColor(color);
  1848. if (!rgba) return null;
  1849.  
  1850. // Convert RGB to HSV to extract hue
  1851. const [r, g, b] = rgba.map(c => c / 255);
  1852. const [hue] = Player.settings.rgbToHsv(r, g, b);
  1853.  
  1854. // Grayscale check (saturation ≈ 0)
  1855. const saturation = Player.settings.rgbToHsv(r, g, b)[1];
  1856. if (saturation < 0.05) return null;
  1857.  
  1858. // Compare hue to yellow (60° in HSV/HSL)
  1859. return (hue * 360) > 60;
  1860. },
  1861.  
  1862. /*
  1863. * color: rgba(255, 255, 255, 1)
  1864. * h: hue, range (-100 — 100)
  1865. * s: saturation, range (-100 — 100)
  1866. * v: value/brightness, range (-100 — 100)
  1867. * a: alpha, decimal range ( 0 — 1 ) and -1 = keep original alpha
  1868. */
  1869. adjustColor: function(color, { h = 0, s = 0, v = 0, a = -1 } = {}) {
  1870. const rgba = Player.settings.parseColor(color);
  1871. if (!rgba) return color;
  1872.  
  1873. // Normalize RGB to [0, 1] and extract alpha (default: 1)
  1874. let [r, g, b, originalA = 1] = rgba;
  1875. r /= 255; g /= 255; b /= 255;
  1876.  
  1877. // Convert to HSV
  1878. const [hue, sat, val] = Player.settings.rgbToHsv(r, g, b);
  1879.  
  1880. // Adjust Hue (handle negative values by looping)
  1881. let newHue = (hue * 360 + h) % 360; // Apply hue shift
  1882. newHue = newHue < 0 ? newHue + 360 : newHue; // Ensure 0-360 range
  1883.  
  1884. // Adjust Saturation & Value (clamped to 0-1)
  1885. const newSat = Math.min(1, Math.max(0, sat + s / 100));
  1886. const newVal = Math.min(1, Math.max(0, val + v / 100));
  1887.  
  1888. // Handle Alpha (if a=-1, keep original; else clamp to [0, 1])
  1889. const newAlpha = a === -1 ? originalA : Math.min(1, Math.max(0, a));
  1890.  
  1891. // Convert back to RGB
  1892. const [newR, newG, newB] = Player.settings.hsvToRgb(newHue, newSat, newVal);
  1893.  
  1894. // Helper function to validate and clamp alpha values
  1895. const clampAlpha = (value) => {
  1896. const num = parseFloat(value);
  1897. return Math.min(1, Math.max(0, isNaN(num) ? 1 : num));
  1898. };
  1899.  
  1900. // Return as RGBA string
  1901. return `rgba(${Math.round(newR * 255)},${Math.round(newG * 255)},${Math.round(newB * 255)},${clampAlpha(newAlpha.toFixed(2))})`;
  1902. },
  1903.  
  1904. /**
  1905. * Mixes two rgba colors with optional weighting and blending mode
  1906. * @param {string} color1 - First color (rgba)
  1907. * @param {string} color2 - Second color (rgba)
  1908. * @param {object} options - Mixing options:
  1909. * - weight: 0-1 (default 0.5, equal blend)
  1910. * @returns {string} Mixed color in rgba() format
  1911. */
  1912. mixColors: function(color1, color2, weight = 0.5) {
  1913. // Parse the input RGBA strings
  1914. const parseRgba = (rgba) => {
  1915. const match = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([0-9.]+)?\)/);
  1916. if (!match) throw new Error("Invalid RGBA format");
  1917. return {
  1918. r: parseInt(match[1]),
  1919. g: parseInt(match[2]),
  1920. b: parseInt(match[3]),
  1921. a: match[4] !== undefined ? parseFloat(match[4]) : 1,
  1922. };
  1923. };
  1924.  
  1925. const c1 = parseRgba(color1);
  1926. const c2 = parseRgba(color2);
  1927.  
  1928. // Linear interpolation function
  1929. const lerp = (a, b, t) => a + (b - a) * t;
  1930.  
  1931. // Mix the colors
  1932. const a = lerp(c1.a, c2.a, weight);
  1933. const r = Math.round(lerp(c1.r * c1.a, c2.r * c2.a, weight) / a);
  1934. const g = Math.round(lerp(c1.g * c1.a, c2.g * c2.a, weight) / a);
  1935. const b = Math.round(lerp(c1.b * c1.a, c2.b * c2.a, weight) / a);
  1936.  
  1937. // Helper function to validate and clamp alpha values
  1938. const clampAlpha = (value) => {
  1939. const num = parseFloat(value);
  1940. return Math.min(1, Math.max(0, isNaN(num) ? 1 : num));
  1941. };
  1942.  
  1943. return `rgba(${r},${g},${b},${clampAlpha(a.toFixed(2))})`;
  1944. },
  1945.  
  1946. rgbToHsv: function(r, g, b) {
  1947. const max = Math.max(r, g, b);
  1948. const min = Math.min(r, g, b);
  1949. let hVal, sVal, vVal = max;
  1950. const d = max - min;
  1951.  
  1952. sVal = max === 0 ? 0 : d / max;
  1953.  
  1954. if (d === 0) {
  1955. hVal = 0;
  1956. } else {
  1957. switch (max) {
  1958. case r: hVal = (g - b) / d + (g < b ? 6 : 0); break;
  1959. case g: hVal = (b - r) / d + 2; break;
  1960. case b: hVal = (r - g) / d + 4; break;
  1961. }
  1962. hVal /= 6;
  1963. }
  1964.  
  1965. return [hVal, sVal, vVal];
  1966. },
  1967.  
  1968. hsvToRgb: function(h, s, v) {
  1969. const c = v * s;
  1970. const x = c * (1 - Math.abs((h / 60) % 2 - 1));
  1971. const m = v - c;
  1972.  
  1973. let r1, g1, b1;
  1974. if (h < 60) [r1, g1, b1] = [c, x, 0];
  1975. else if (h < 120) [r1, g1, b1] = [x, c, 0];
  1976. else if (h < 180) [r1, g1, b1] = [0, c, x];
  1977. else if (h < 240) [r1, g1, b1] = [0, x, c];
  1978. else if (h < 300) [r1, g1, b1] = [x, 0, c];
  1979. else [r1, g1, b1] = [c, 0, x];
  1980.  
  1981. return [r1 + m, g1 + m, b1 + m];
  1982. },
  1983.  
  1984. /**
  1985. * Update a setting.
  1986. */
  1987. set: function(property, value, {
  1988. bypassSave,
  1989. bypassRender,
  1990. silent
  1991. } = {}) {
  1992. const previousValue = _get(Player.config, property);
  1993. if (previousValue === value) {
  1994. return;
  1995. }
  1996. _set(Player.config, property, value);
  1997. !silent && Player.trigger('config', property, value, previousValue);
  1998. !silent && Player.trigger('config:' + property, value, previousValue);
  1999. !bypassSave && Player.settings.save();
  2000. !bypassRender && Player.settings.findDefault(property).showInSettings && Player.settings.render();
  2001. },
  2002.  
  2003. /**
  2004. * Reset a setting to the default value
  2005. */
  2006. reset: function(property) {
  2007. let settingConfig = Player.settings.findDefault(property);
  2008. Player.set(property, settingConfig.default);
  2009. Player.display.updateStylesheet();
  2010. //Player.settings.render();
  2011. },
  2012.  
  2013. /**
  2014. * Persist the player settings.
  2015. */
  2016. save: function() {
  2017. try {
  2018. // Filter settings that have been modified from the default.
  2019. const settings = settingsConfig.reduce(function _handleSetting(settings, setting) {
  2020. if (setting.settings) {
  2021. setting.settings.forEach(subSetting => _handleSetting(settings, {
  2022. property: setting.property,
  2023. default: setting.default,
  2024. ...subSetting
  2025. }));
  2026. } else {
  2027. const userVal = _get(Player.config, setting.property);
  2028. if (userVal !== undefined && userVal !== setting.default) {
  2029. _set(settings, setting.property, userVal);
  2030. }
  2031. }
  2032. return settings;
  2033. }, {});
  2034. // Show the playlist or image view on load, whichever was last shown.
  2035. settings.viewStyle = Player.playlist._lastView;
  2036. // Store the player version with the settings.
  2037. settings.VERSION = window.VERSION;
  2038. // Save the settings.
  2039. return GM.setValue('settings', JSON.stringify(settings));
  2040. } catch (err) {
  2041. Player.logError('There was an error saving the sound player settings. Please check the console for details.');
  2042. console.error('[8chan sounds player]', err);
  2043. }
  2044. },
  2045.  
  2046. /**
  2047. * Restore the saved player settings.
  2048. */
  2049. load: async function() {
  2050. try {
  2051. let settings = await GM.getValue('settings') || await GM.getValue(ns + '.settings');
  2052. if (settings) {
  2053. Player.settings.apply(settings, {
  2054. bypassSave: true,
  2055. silent: true
  2056. });
  2057. }
  2058. } catch (err) {
  2059. Player.logError('There was an error loading the sound player settings. Please check the console for details.');
  2060. console.error('[8chan sounds player]', err);
  2061. }
  2062. },
  2063.  
  2064. apply: function(settings, opts = {}) {
  2065. if (typeof settings === 'string') {
  2066. settings = JSON.parse(settings);
  2067. }
  2068. settingsConfig.forEach(function _handleSetting(setting) {
  2069. if (setting.settings) {
  2070. return setting.settings.forEach(subSetting => _handleSetting({
  2071. property: setting.property,
  2072. default: setting.default,
  2073. ...subSetting
  2074. }));
  2075. }
  2076. if (opts.ignore && opts.ignore.includes(opts.property)) {
  2077. return;
  2078. }
  2079. const value = _get(settings, setting.property, opts.applyDefault ? setting.default : undefined);
  2080. if (value !== undefined) {
  2081. Player.set(setting.property, value, opts);
  2082. }
  2083. });
  2084. },
  2085.  
  2086. /**
  2087. * Find a setting in the default configuration.
  2088. */
  2089. findDefault: function(property) {
  2090. let settingConfig;
  2091. settingsConfig.find(function(setting) {
  2092. if (setting.property === property) {
  2093. return settingConfig = setting;
  2094. }
  2095. if (setting.settings) {
  2096. let subSetting = setting.settings.find(_setting => _setting.property === property);
  2097. return subSetting && (settingConfig = {
  2098. ...setting,
  2099. settings: null,
  2100. ...subSetting
  2101. });
  2102. }
  2103. return false;
  2104. });
  2105. return settingConfig || {
  2106. property
  2107. };
  2108. },
  2109.  
  2110. /**
  2111. * Toggle whether the player or settings are displayed.
  2112. */
  2113. toggle: function(e) {
  2114. e && e.preventDefault();
  2115. // Blur anything focused so the change is applied.
  2116. let focused = Player.$(`.${ns}-settings :focus`);
  2117. focused && focused.blur();
  2118. if (Player.config.viewStyle === 'settings') {
  2119. Player.playlist.restore();
  2120. } else {
  2121. Player.display.setViewStyle('settings');
  2122. }
  2123. },
  2124.  
  2125. /**
  2126. * Handle the user making a change in the settings view.
  2127. */
  2128. handleChange: function(e) {
  2129. try {
  2130. const input = e.eventTarget;
  2131. const property = input.getAttribute('data-property');
  2132. if (!property) {
  2133. return;
  2134. }
  2135. let settingConfig = Player.settings.findDefault(property);
  2136.  
  2137. // Get the new value of the setting.
  2138. const currentValue = _get(Player.config, property);
  2139. let newValue = input[input.getAttribute('type') === 'checkbox' ? 'checked' : 'value'];
  2140.  
  2141. if (settingConfig.parse) {
  2142. newValue = _get(Player, settingConfig.parse)(newValue);
  2143. }
  2144. if (settingConfig && settingConfig.split) {
  2145. newValue = newValue.split(decodeURIComponent(settingConfig.split));
  2146. }
  2147.  
  2148. // Not the most stringent check but enough to avoid some spamming.
  2149. if (currentValue !== newValue) {
  2150. // Update the setting.
  2151. Player.set(property, newValue, {
  2152. bypassRender: true
  2153. });
  2154.  
  2155. // Update the stylesheet reflect any changes.
  2156. if (settingConfig.updateStylesheet) {
  2157. Player.display.updateStylesheet();
  2158. }
  2159. }
  2160.  
  2161. // Run any handler required by the value changing
  2162. settingConfig && settingConfig.handler && _get(Player, settingConfig.handler, () => null)(newValue);
  2163. } catch (err) {
  2164. Player.logError('There was an error updating the setting. Please check the console for details.');
  2165. console.error('[8chan sounds player]', err);
  2166. }
  2167. },
  2168.  
  2169. /**
  2170. * Converts a key event in an input to a string representation set as the input value.
  2171. */
  2172. handleKeyChange: function(e) {
  2173. e.preventDefault();
  2174. if (e.key === 'Shift' || e.key === 'Control' || e.key === 'Meta') {
  2175. return;
  2176. }
  2177. e.eventTarget.value = Player.hotkeys.stringifyKey(e);
  2178. },
  2179.  
  2180. /**
  2181. * Handle an action link next to a heading being clicked.
  2182. */
  2183. handleAction: function(e) {
  2184. e.preventDefault();
  2185. const property = e.eventTarget.getAttribute('data-property');
  2186. const handlerName = e.eventTarget.getAttribute('data-handler');
  2187. const handler = _get(Player, handlerName);
  2188. handler && handler(property);
  2189. },
  2190.  
  2191. /**
  2192. * Encode the decoded input.
  2193. */
  2194. _handleDecoded: function(e) {
  2195. Player.$(`.${ns}-encoded-input`).value = encodeURIComponent(e.eventTarget.value);
  2196. },
  2197.  
  2198. /**
  2199. * Decode the encoded input.
  2200. */
  2201. _handleEncoded: function(e) {
  2202. Player.$(`.${ns}-decoded-input`).value = decodeURIComponent(e.eventTarget.value);
  2203. }
  2204. };
  2205.  
  2206.  
  2207. }),
  2208. /* 6 - Playback Controls
  2209. • Core audio functions:
  2210. o play()/pause()/togglePlay()
  2211. o next()/previous(): Track navigation
  2212. o _movePlaying(): Handles repeat modes
  2213. • UI controls:
  2214. o Seek bar handling
  2215. o Volume control
  2216. o Progress updates
  2217. • Video sync for webm files
  2218. */
  2219. (function(module, exports) {
  2220. const videoFileExtRE = /\.(webm|mp4|m4v|ogv|avi|mpeg|mpg|mpe|m1v|m2v|mov|wmv)$/i;
  2221. const audioFileExtRE = /\.(mp3|m4a|m4b|flac|ogg|oga|opus|mp2|mpega|wav|aac)$/i;
  2222. const videoMimeRE = /^video\/.+$/;
  2223. const audioMimeRE = /^audio\/.+$/;
  2224. const progressBarStyleSheets = {};
  2225. const pendingRequests = new Set();
  2226. let syncInterval;
  2227.  
  2228. module.exports = {
  2229. atRoot: ['togglePlay', 'play', 'pause', 'next', 'previous'],
  2230.  
  2231. delegatedEvents: {
  2232. click: {
  2233. [`.${ns}-previous-button`]: () => Player.previous(),
  2234. [`.${ns}-play-button`]: 'togglePlay',
  2235. [`.${ns}-next-button`]: () => Player.next(),
  2236. [`.${ns}-seek-bar`]: 'controls.handleSeek',
  2237. [`.${ns}-volume-bar`]: 'controls.handleVolume',
  2238. [`.${ns}-fullscreen-button`]: 'display.toggleFullScreen',
  2239. [`.${ns}-media:not(.${ns}-pip) .${ns}-image-link`]: 'togglePlay',
  2240. },
  2241. mousedown: {
  2242. [`.${ns}-seek-bar`]: () => Player._seekBarDown = true,
  2243. [`.${ns}-volume-bar`]: () => Player._volumeBarDown = true
  2244. },
  2245. mousemove: {
  2246. [`.${ns}-seek-bar`]: e => Player._seekBarDown && Player.controls.handleSeek(e),
  2247. [`.${ns}-volume-bar`]: e => Player._volumeBarDown && Player.controls.handleVolume(e)
  2248. }
  2249. },
  2250.  
  2251. undelegatedEvents: {
  2252. ended: {
  2253. [`.${ns}-video`]: 'controls.handleSoundEnded'
  2254. },
  2255. mouseleave: {
  2256. [`.${ns}-seek-bar`]: e => Player._seekBarDown && Player.controls.handleSeek(e),
  2257. [`.${ns}-volume-bar`]: e => Player._volumeBarDown && Player.controls.handleVolume(e)
  2258. },
  2259. mouseup: {
  2260. body: () => {
  2261. Player._seekBarDown = false;
  2262. Player._volumeBarDown = false;
  2263. }
  2264. }
  2265. },
  2266.  
  2267. soundEvents: {
  2268. ended: 'controls.handleSoundEnded',
  2269. pause: 'controls.handlePlaybackState',
  2270. play: 'controls.handlePlaybackState',
  2271. seeked: 'controls.handlePlaybackState',
  2272. playing: 'controls.handlePlaybackState',
  2273. waiting: 'controls.handlePlaybackState',
  2274. timeupdate: 'controls.updateDuration',
  2275. loadedmetadata: 'controls.updateDuration',
  2276. durationchange: 'controls.updateDuration',
  2277. volumechange: 'controls.updateVolume',
  2278. loadstart: 'controls.pollForLoading',
  2279. error: 'controls.handleSoundError',
  2280. },
  2281.  
  2282. audioEvents: {
  2283. ended: 'controls.handleSoundEnded',
  2284. pause: 'controls.handlePlaybackState',
  2285. play: 'controls.handlePlaybackState',
  2286. //seeked: 'controls.handlePlaybackState',
  2287. //playing: 'controls.handlePlaybackState',
  2288. //waiting: 'controls.handlePlaybackState',
  2289. timeupdate: 'controls.updateDuration',
  2290. //loadedmetadata: 'controls.updateDuration',
  2291. durationchange: 'controls.updateDuration',
  2292. //volumechange: 'controls.updateVolume',
  2293. //loadstart: 'controls.pollForLoading',
  2294. },
  2295.  
  2296. initialize: function() {
  2297. Player.on('show', () => Player._hiddenWhilePolling && Player.controls.pollForLoading());
  2298. Player.on('hide', () => {
  2299. Player._hiddenWhilePolling = !!Player._loadingPoll;
  2300. Player.controls.stopPollingForLoading();
  2301. });
  2302.  
  2303. // Initialize loop state based on current repeat mode
  2304. const updateLoop = () => {
  2305. const video = document.querySelector(`.${ns}-video`);
  2306.  
  2307. // if durations don't equal ±2 seconds difference.
  2308. if (window.Slave !== undefined && (Math.abs(window.Master.duration - window.Slave.duration) > 2)) {
  2309. video.loop = true;
  2310. Player.audio.loop = Player.config.repeat === 'one';
  2311. return;
  2312. }
  2313.  
  2314. video.loop = Player.config.repeat === 'one';
  2315. Player.audio.loop = Player.config.repeat === 'one';
  2316. };
  2317.  
  2318. // Listen for repeat mode changes through Player events
  2319. Player.on('config:repeat', updateLoop);
  2320.  
  2321. document.addEventListener('visibilitychange', () => {
  2322. // video starts to lag when window is in background, this should get it back to normal speed on tab in + should fix sync
  2323. if (!document.hidden && Player.playing && window.Master !== undefined && !window.Master.paused) {
  2324. const video = document.querySelector(`.${ns}-video`);
  2325.  
  2326. if (isFinite(window.Master.duration) && window.Slave !== undefined && (Math.abs(window.Master.duration - video.duration) < 2) || isFinite(window.Master.duration) && window.Slave === undefined) {
  2327. // Try to resume playback when tab becomes visible
  2328. const currentTime = window.Master.currentTime;
  2329. window.Master.currentTime = 0;
  2330. video.currentTime = 0;
  2331. window.Master.currentTime = currentTime;
  2332. video.currentTime = currentTime;
  2333. }
  2334.  
  2335. window.Master.play().catch(() => {});
  2336. video.play().catch(() => {});
  2337. Player.controls.handlePlaybackState(); // Resync UI
  2338. }
  2339. });
  2340.  
  2341. Player.on('rendered', () => {
  2342. // Keep track of heavily updated elements.
  2343. Player.ui.currentTimeBar = Player.$(`.${ns}-seek-bar .${ns}-current-bar`);
  2344. Player.ui.loadedBar = Player.$(`.${ns}-seek-bar .${ns}-loaded-bar`);
  2345.  
  2346. Player.on('rendered', () => {
  2347. Player.ui.currentTimeBar = Player.$(`.${ns}-seek-bar .${ns}-current-bar`);
  2348. Player.ui.loadedBar = Player.$(`.${ns}-seek-bar .${ns}-loaded-bar`);
  2349. });
  2350.  
  2351. // video event listeners
  2352. const video = document.querySelector(`.${ns}-video`);
  2353. if (video) {
  2354. Object.entries(Player.controls.soundEvents).forEach(([event, handler]) => {
  2355. // Handle both string paths and direct function references
  2356. const handlerFn = typeof handler === 'function'
  2357. ? handler
  2358. : _get(Player, handler);
  2359. video.addEventListener(event, handlerFn);
  2360. });
  2361. }
  2362.  
  2363. // audio element event listeners
  2364. Object.entries(Player.controls.soundEvents).forEach(([event, handler]) => {
  2365. Player.audio.addEventListener(event, Player.controls[handler]);
  2366. });
  2367.  
  2368. // Update repeat mode when player is rendered
  2369. video.loop = Player.config.repeat === 'one';
  2370. Player.audio.loop = Player.config.repeat === 'one';
  2371.  
  2372. // Restore volume value from the previous session.
  2373. Player.audio.volume = parseFloat(Player.config.volumeValue ) || '1';
  2374. video.volume = parseFloat(Player.config.volumeValue) || '1';
  2375.  
  2376. // Add stylesheets to adjust the progress indicator of the seekbar and volume bar.
  2377. document.head.appendChild(progressBarStyleSheets[`.${ns}-seek-bar`] = document.createElement('style'));
  2378. document.head.appendChild(progressBarStyleSheets[`.${ns}-volume-bar`] = document.createElement('style'));
  2379. Player.controls.updateVolume();
  2380. });
  2381. },
  2382.  
  2383. /**
  2384. * Switching being playing and paused.
  2385. */
  2386. togglePlay: function() {
  2387. // Return early if currently loading
  2388. if (window.isLoading) return;
  2389.  
  2390. if (!Player.playing) {
  2391. if (Player.sounds.length) {
  2392. return Player.play(Player.sounds[0]);
  2393. }
  2394. return;
  2395. }
  2396.  
  2397. const video = document.querySelector(`.${ns}-video`);
  2398.  
  2399. if (window.Master !== undefined && window.Master.ended) {
  2400. window.Master.currentTime = 0;
  2401. video.currentTime = 0;
  2402. window.Master.play();
  2403. video.play().catch(() => {});
  2404. } else if (window.Master !== undefined && window.Master.paused) {
  2405. video.currentTime = window.Master.currentTime;
  2406. window.Master.play();
  2407. video.play().catch(() => {});
  2408. } else {
  2409. if (window.Master !== undefined) window.Master.pause();
  2410. if (video) video.pause();
  2411. }
  2412.  
  2413. Player.controls.handlePlaybackState();
  2414. },
  2415.  
  2416. updatePlayButtonState: function() {
  2417. const buttons = document.querySelectorAll(`.${ns}-play-button, .${ns}-seek-bar`);
  2418. buttons.forEach(button => {
  2419. button.disabled = window.isLoading;
  2420. button.style.opacity = window.isLoading ? '0.5' : '1';
  2421. button.style.cursor = window.isLoading ? 'not-allowed' : 'pointer';
  2422. });
  2423. },
  2424.  
  2425. // Function to safely get file extension (handles multiple dots in filename)
  2426. getFileExtension: function(filename) {
  2427. // Handle edge cases: no extension, hidden files, or filenames ending with dot
  2428. if (!filename || filename.indexOf('.') === -1 || filename.endsWith('.')) {
  2429. return '';
  2430. }
  2431. return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase();
  2432. },
  2433.  
  2434. detectMimeType: function(url, arrayBuffer, responseType) {
  2435. if(audioMimeRE.test(responseType)) return responseType;
  2436. if(videoMimeRE.test(responseType)) return responseType;
  2437.  
  2438. const extension = Player.controls.getFileExtension(url);
  2439. const bytes = new Uint8Array(arrayBuffer);
  2440.  
  2441. // Check by file signature (magic numbers)
  2442.  
  2443. // MKV / WebM
  2444. if (bytes.length >= 4 &&
  2445. bytes[0] === 0x1A && bytes[1] === 0x45 && bytes[2] === 0xDF && bytes[3] === 0xA3) {
  2446. // Ideally parse to find DocType (e.g., webm or matroska)
  2447. return /*extension === 'webm' ? */'video/webm'/* : 'video/x-matroska'*/;
  2448. }
  2449.  
  2450. // MP4/M4A/M4V/M4B (MPEG-4 containers)
  2451. if (bytes.length >= 8 &&
  2452. ((bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) || // ftyp
  2453. (bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x00 &&
  2454. (bytes[3] === 0x18 || bytes[3] === 0x20) && bytes[4] === 0x66 &&
  2455. bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70))) {
  2456. // Check for specific MP4 subtypes
  2457. if (bytes.length >= 12) {
  2458. if (bytes[8] === 0x4D && bytes[9] === 0x34 && bytes[10] === 0x41 && bytes[11] === 0x20) {
  2459. return 'audio/mp4'; // M4A
  2460. }
  2461. if (bytes[8] === 0x4D && bytes[9] === 0x34 && bytes[10] === 0x56 && bytes[11] === 0x20) {
  2462. return 'video/mp4'; // M4V
  2463. }
  2464. if (bytes[8] === 0x4D && bytes[9] === 0x34 && bytes[10] === 0x42 && bytes[11] === 0x20) {
  2465. return 'audio/mp4'; // M4B (audiobook format, same as M4A)
  2466. }
  2467. if (bytes[8] === 0x71 && bytes[9] === 0x74 && bytes[10] === 0x20 && bytes[11] === 0x20) {
  2468. return 'video/quicktime'; // MOV (QuickTime)
  2469. }
  2470. }
  2471. return 'video/mp4'; // default MP4
  2472. }
  2473.  
  2474. // FLAC
  2475. if (bytes.length >= 4 &&
  2476. bytes[0] === 0x66 &&
  2477. bytes[1] === 0x4C &&
  2478. bytes[2] === 0x61 &&
  2479. bytes[3] === 0x43) {
  2480. return 'audio/flac';
  2481. }
  2482.  
  2483. // OGG (including OGV, OGA, OPUS)
  2484. if (bytes.length >= 4 &&
  2485. bytes[0] === 0x4F &&
  2486. bytes[1] === 0x67 &&
  2487. bytes[2] === 0x67 &&
  2488. bytes[3] === 0x53) {
  2489. // Could be audio or video OGG
  2490. return extension === 'ogv' ? 'video/ogg' : 'audio/ogg';
  2491. }
  2492.  
  2493. // AVI
  2494. if (bytes.length >= 12 &&
  2495. bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 && // RIFF
  2496. bytes[8] === 0x41 && bytes[9] === 0x56 && bytes[10] === 0x49 && bytes[11] === 0x20) { // AVI
  2497. return 'video/x-msvideo';
  2498. }
  2499.  
  2500. // WAV
  2501. if (bytes.length >= 12 &&
  2502. bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 && // RIFF
  2503. bytes[8] === 0x57 && bytes[9] === 0x41 && bytes[10] === 0x56 && bytes[11] === 0x45) { // WAVE
  2504. return 'audio/wav';
  2505. }
  2506.  
  2507. // MOV (QuickTime)
  2508. if (bytes.length >= 8 &&
  2509. ((bytes[4] === 0x6D && bytes[5] === 0x6F && bytes[6] === 0x6F && bytes[7] === 0x76) || // moov
  2510. (bytes[4] === 0x66 && bytes[5] === 0x72 && bytes[6] === 0x65 && bytes[7] === 0x65))) { // free
  2511. return 'video/quicktime';
  2512. }
  2513.  
  2514. // WMV/ASF
  2515. if (bytes.length >= 16 &&
  2516. bytes[0] === 0x30 && bytes[1] === 0x26 && bytes[2] === 0xB2 && bytes[3] === 0x75 &&
  2517. bytes[4] === 0x8E && bytes[5] === 0x66 && bytes[6] === 0xCF && bytes[7] === 0x11 &&
  2518. bytes[8] === 0xA6 && bytes[9] === 0xD9 && bytes[10] === 0x00 && bytes[11] === 0xAA &&
  2519. bytes[12] === 0x00 && bytes[13] === 0x62 && bytes[14] === 0xCE && bytes[15] === 0x6C) {
  2520. return extension === 'wmv' ? 'video/x-ms-wmv' : 'video/x-ms-asf';
  2521. }
  2522.  
  2523. // MKV (Matroska)
  2524. if (bytes.length >= 4 &&
  2525. bytes[0] === 0x1A && bytes[1] === 0x45 && bytes[2] === 0xDF && bytes[3] === 0xA3) {
  2526. return 'video/x-matroska';
  2527. }
  2528.  
  2529. // MPEG (MP3, MP2, MPEG video)
  2530. if (bytes.length >= 3) {
  2531. // MP3 with ID3 tag
  2532. if (bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33) {
  2533. return 'audio/mpeg';
  2534. }
  2535.  
  2536. // MPEG audio (MP3, MP2) - frame sync
  2537. if ((bytes[0] === 0xFF) && ((bytes[1] & 0xE0) === 0xE0)) {
  2538. // Check layer bits (bits 1-2 of byte 1)
  2539. const layer = (bytes[1] & 0x06) >> 1;
  2540. // Layer 3 (MP3) or Layer 2 (MP2)
  2541. return layer === 3 ? 'audio/mpeg' : 'audio/mpeg'; // MP2 also uses audio/mpeg
  2542. }
  2543.  
  2544. // MPEG video
  2545. if (bytes.length >= 4 &&
  2546. bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 &&
  2547. (bytes[3] >= 0xB0 && bytes[3] <= 0xBF)) {
  2548. return 'video/mpeg';
  2549. }
  2550. }
  2551.  
  2552. // 3GP/3G2 (mobile video formats)
  2553. if (bytes.length >= 12 &&
  2554. bytes[4] === 0x66 && bytes[5] === 0x74 &&
  2555. bytes[6] === 0x79 && bytes[7] === 0x70) { // 'ftyp'
  2556.  
  2557. const brand = String.fromCharCode(bytes[8], bytes[9], bytes[10], bytes[11]);
  2558.  
  2559. // Known 3GP/3G2 brands
  2560. const known3GPBrands = ['3gp4', '3gp5', '3g2a', '3g2b', '3gr6', '3gs7', '3ge6', '3gg6'];
  2561.  
  2562. if (known3GPBrands.includes(brand)) {
  2563. return 'video/3gpp';
  2564. }
  2565. }
  2566.  
  2567. // AAC (Advanced Audio Coding)
  2568. if (bytes.length >= 2 &&
  2569. (bytes[0] === 0xFF && (bytes[1] & 0xF6) === 0xF0)) {
  2570. return 'audio/aac';
  2571. }
  2572.  
  2573. // Fallback to extension-based detection
  2574. switch(extension) {
  2575. case 'webm': return 'video/webm';
  2576. case 'mp4': return 'video/mp4';
  2577. case 'm4a': case 'm4b': return 'audio/mp4';
  2578. case 'm4v': return 'video/mp4';
  2579. case 'flac': return 'audio/flac';
  2580. case 'ogg': case 'oga': return 'audio/ogg';
  2581. case 'ogv': return 'video/ogg';
  2582. case 'opus': return 'audio/ogg';
  2583. case 'avi': return 'video/x-msvideo';
  2584. case 'asx': return 'video/x-ms-asf'; // Advanced Stream Redirector
  2585. case 'mpeg': case 'mpg': case 'mpe': case 'm1v': case 'm2v': return 'video/mpeg';
  2586. case 'mp3': case 'mpega': case 'mp2': return 'audio/mpeg';
  2587. case 'm3u': return 'application/x-mpegurl'; // Playlist file
  2588. default: return 'audio/mpeg'; // default fallback
  2589. }
  2590. },
  2591.  
  2592. BlobXmlHttpRequest: function (src) {
  2593. return new Promise((resolve, reject) => {
  2594. const requestDetails = {
  2595. src,
  2596. resolve,
  2597. reject,
  2598. aborted: false
  2599. };
  2600.  
  2601. pendingRequests.add(requestDetails);
  2602.  
  2603. GM.xmlHttpRequest({
  2604. method: 'GET',
  2605. url: src,
  2606. responseType: 'blob',
  2607. onload: function(response) {
  2608. if (requestDetails.aborted) return; // Skip if aborted
  2609. pendingRequests.delete(requestDetails);
  2610.  
  2611. if (response.status >= 400) {
  2612. console.error(`[8chan sounds player] Failed to fetch media; status: ${response.status}`);
  2613. reject(new Error(`HTTP ${response.status}`));
  2614. } else {
  2615. resolve(response);
  2616. }
  2617. },
  2618. onerror: function(error) {
  2619. pendingRequests.delete(requestDetails);
  2620. reject(error);
  2621. },
  2622. ontimeout: function() {
  2623. pendingRequests.delete(requestDetails);
  2624. reject(new Error('Request timed out'));
  2625. },
  2626. timeout: 0
  2627. });
  2628. });
  2629. },
  2630.  
  2631. /**
  2632. * Cancel a specific request by URL
  2633. */
  2634. cancelRequest: function(src) {
  2635. pendingRequests.forEach(request => {
  2636. if (request.src === src) {
  2637. request.aborted = true;
  2638. request.reject(new Error('Request cancelled by user'));
  2639. pendingRequests.delete(request);
  2640. }
  2641. });
  2642. },
  2643.  
  2644. /**
  2645. * Cancel all pending requests
  2646. */
  2647. cancelAllRequests: function() {
  2648. pendingRequests.forEach(request => {
  2649. request.aborted = true;
  2650. request.reject(new Error('All requests cancelled'));
  2651. });
  2652. pendingRequests.clear();
  2653. },
  2654.  
  2655. BlobReader: function(blob) {
  2656. return new Promise((resolve, reject) => {
  2657. const reader = new FileReader();
  2658. reader.onload = () => {
  2659. // Extract only the base64 data after the comma
  2660. const dataUrl = reader.result;
  2661. const base64Data = dataUrl.split(',')[1]; // Split at comma and take the second part
  2662. resolve(base64Data);
  2663. };
  2664. reader.onerror = reject;
  2665. reader.readAsDataURL(blob);
  2666. });
  2667. },
  2668.  
  2669. /**
  2670. * Wait for audio to be ready to play
  2671. */
  2672. waitForAudioReady: function() {
  2673. return new Promise((resolve, reject) => {
  2674. if (!Player.audio) {
  2675. return reject(new Error('Player.audio element not found'));
  2676. }
  2677.  
  2678. // Check if already ready
  2679. if (Player.audio.readyState >= 3) {
  2680. return resolve();
  2681. }
  2682.  
  2683. const onReady = () => {
  2684. cleanup();
  2685. resolve();
  2686. };
  2687.  
  2688. const onError = (err) => {
  2689. cleanup();
  2690. reject(err);
  2691. };
  2692.  
  2693. const cleanup = () => {
  2694. Player.audio.removeEventListener('loadeddata', onReady);
  2695. Player.audio.removeEventListener('error', onError);
  2696. };
  2697.  
  2698. Player.audio.addEventListener('loadeddata', onReady);
  2699. Player.audio.addEventListener('error', onError);
  2700. });
  2701. },
  2702.  
  2703. /**
  2704. * Wait for video to be ready to play
  2705. */
  2706. waitForVideoReady: function() {
  2707. return new Promise((resolve, reject) => {
  2708. const video = document.querySelector(`.${ns}-video`);
  2709. if (!video) {
  2710. return reject(new Error('Video element not found'));
  2711. }
  2712.  
  2713. // Check if already ready
  2714. if (video.readyState >= 4) {
  2715. return resolve();
  2716. }
  2717.  
  2718. const onReady = () => {
  2719. cleanup();
  2720. resolve();
  2721. };
  2722.  
  2723. const onError = (err) => {
  2724. cleanup();
  2725. reject(err);
  2726. };
  2727.  
  2728. const cleanup = () => {
  2729. video.removeEventListener('loadeddata', onReady);
  2730. video.removeEventListener('error', onError);
  2731. };
  2732.  
  2733. video.addEventListener('loadeddata', onReady);
  2734. video.addEventListener('error', onError);
  2735. });
  2736. },
  2737.  
  2738.  
  2739. /**
  2740. * Start playback.
  2741. */
  2742. play: async function(sound) {
  2743. const video = document.querySelector(`.${ns}-video`);
  2744. const image = document.querySelector(`.${ns}-image`);
  2745.  
  2746. // if play(sound) and previous play(sound) equal just reset currentTime
  2747. if (Player.playing !== undefined && window.Master !== undefined && sound.id === Player.playing.id) {
  2748. window.Master.currentTime = 0;
  2749. video.currentTime = 0;
  2750. window.Master.play().catch(() => {});
  2751. video.play().catch(() => {});
  2752. Player.controls.handlePlaybackState(); // Resync UI
  2753. return;
  2754. }
  2755.  
  2756. Player.controls.cancelAllRequests();
  2757.  
  2758. window.mediaStatus = undefined; Player.header.render();
  2759. window.isLoading = true;
  2760.  
  2761. if (!sound && !Player.playing && Player.sounds.length) {
  2762. sound = Player.sounds[0];
  2763. }
  2764. if (!sound) {
  2765. window.isLoading = false;
  2766. return;
  2767. }
  2768.  
  2769. //console.log(sound);
  2770.  
  2771. window.Master = undefined;
  2772. window.Slave = undefined;
  2773.  
  2774. // Clear previous playback
  2775. if (Player.playing) Player.playing.playing = false;
  2776.  
  2777. // Reset media elements completely
  2778. video.pause();
  2779. video.removeAttribute('src');
  2780. video.load();
  2781. video.currentTime = 0;
  2782. Player.audio.pause();
  2783. Player.audio.removeAttribute('src');
  2784. Player.audio.load();
  2785. Player.audio.currentTime = 0;
  2786.  
  2787. Player.controls.updatePlayButtonState();
  2788.  
  2789. try {
  2790. sound.playing = true;
  2791. Player.playing = sound;
  2792. await Player.trigger('playsound', sound);
  2793.  
  2794. // Case 1: hasSoundTag and the sound tag is audio (.mp3, .ogg, .m4a, ...)
  2795. if (sound.hasSoundTag && !sound.isVideo) {
  2796. window.mediaStatus = "Loading"; Player.header.render();
  2797. window.Master = Player.audio;
  2798. window.Slave = video;
  2799. // First try with GM.xmlHttpRequest
  2800. const response = await Player.controls.BlobXmlHttpRequest(sound.src);
  2801. //console.log(response.response); console.log('response.type '+response.response.type); console.log('response.status '+response.status);
  2802.  
  2803. if (response.status === 200) {
  2804. const rawBase64 = await Player.controls.BlobReader(response.response);
  2805. const mimeType = await Player.controls.detectMimeType(sound.src, rawBase64, response.response.type);
  2806. Player.audio.src = `data:${mimeType};base64,${rawBase64}`;
  2807. video.muted = true;
  2808.  
  2809. // Wait for Player.audio to be ready
  2810. await Player.controls.waitForAudioReady();
  2811.  
  2812. if (!isFinite(window.Master.duration)) {
  2813. // Try to estimate from buffered data
  2814. if (window.Master.buffered && window.Master.buffered.length > 0) {
  2815. window.Master.duration = window.Master.buffered.end(window.Master.buffered.length - 1);
  2816. }
  2817. }
  2818.  
  2819. } else {
  2820. console.error(new Error('[8chan sounds player] Failed to fetch via GM_xmlhttpRequest, trying fallback:'));
  2821. window.mediaStatus = "Error"; Player.header.render();
  2822. Player.audio.pause();
  2823. Player.audio.removeAttribute('src');
  2824. Player.audio.load();
  2825. window.Master = video;
  2826. video.muted = false;
  2827. window.Slave = undefined;
  2828. }
  2829.  
  2830. // Handle video/image element carefully for Case 1
  2831. const imageIsVideo = videoFileExtRE.test(sound.filename); // Check if sound.image is actually a supported video format
  2832. if (imageIsVideo) {
  2833. video.src = sound.image; // Use .image for video if it's a supported format
  2834.  
  2835. // Wait for video to be ready
  2836. await Player.controls.waitForVideoReady();
  2837.  
  2838. await video.play().catch(e => {
  2839. console.error('[8chan sounds player] Video playback failed, falling back to empty source:', e);
  2840. video.pause();
  2841. video.removeAttribute('src');
  2842. video.load();
  2843. window.Slave = undefined;
  2844. });
  2845. } else {
  2846. video.pause();
  2847. video.removeAttribute('src');
  2848. video.load();
  2849. window.Slave = undefined;
  2850. }
  2851.  
  2852. // Start playback and Initial sync
  2853. if (response.status === 200) {
  2854. await Player.audio.play();
  2855. Player.controls.syncPlayback();
  2856. // Start sync interval
  2857. if (syncInterval) clearInterval(syncInterval);
  2858. syncInterval = setInterval(() => Player.controls.syncPlayback(), 1000);
  2859. }
  2860. }
  2861.  
  2862. // Case 2: hasSoundTag and the sound tag is video (.webm, .mp4)
  2863. else if (sound.hasSoundTag && sound.isVideo) {
  2864. window.mediaStatus = "Loading"; Player.header.render();
  2865. window.Master = video;
  2866. // First try with GM.xmlHttpRequest
  2867. const response = await Player.controls.BlobXmlHttpRequest(sound.src);
  2868. //console.log(response.response); console.log('response.type '+response.response.type); console.log('response.status '+response.status);
  2869.  
  2870. if (response.status === 200) {
  2871. const rawBase64 = await Player.controls.BlobReader(response.response);
  2872. const mimeType = await Player.controls.detectMimeType(sound.src, rawBase64, response.response.type);
  2873. video.src = `data:${mimeType};base64,${rawBase64}`;
  2874. video.muted = false;
  2875.  
  2876. // Wait for video to be ready
  2877. await Player.controls.waitForVideoReady();
  2878.  
  2879. if (!isFinite(window.Master.duration)) {
  2880. // Try to estimate from buffered data
  2881. if (window.Master.buffered && window.Master.buffered.length > 0) {
  2882. window.Master.duration = window.Master.buffered.end(window.Master.buffered.length - 1);
  2883. }
  2884. }
  2885.  
  2886. // Start playback
  2887. await video.play();
  2888.  
  2889. } else {
  2890. console.error(new Error('[8chan sounds player] Failed to fetch via GM_xmlhttpRequest, trying fallback:'));
  2891. window.mediaStatus = "Error"; Player.header.render();
  2892. video.muted = false;
  2893. // Fallback to direct video playback
  2894. const imageIsVideo = videoFileExtRE.test(sound.filename); // Check if sound.image is actually a supported video format
  2895. if (imageIsVideo) {
  2896. video.src = sound.image; // Use .image for video if it's a supported format
  2897.  
  2898. // Wait for video to be ready
  2899. await Player.controls.waitForVideoReady();
  2900.  
  2901. if (!isFinite(window.Master.duration)) {
  2902. // Try to estimate from buffered data
  2903. if (window.Master.buffered && window.Master.buffered.length > 0) {
  2904. window.Master.duration = window.Master.buffered.end(window.Master.buffered.length - 1);
  2905. }
  2906. }
  2907.  
  2908. await video.play().catch(e => {
  2909. console.error('[8chan sounds player] Video playback failed, falling back to empty source:', e);
  2910. video.pause();
  2911. video.removeAttribute('src');
  2912. video.load();
  2913. window.Master = undefined;
  2914. });
  2915. } else {
  2916. video.pause();
  2917. video.removeAttribute('src');
  2918. video.load();
  2919. window.Master = undefined;
  2920. }
  2921. }
  2922. }
  2923.  
  2924. // Case 3: doesn't have hasSoundTag and is video
  2925. else if (!sound.hasSoundTag && sound.isVideo) {
  2926. window.mediaStatus = "Loading"; Player.header.render();
  2927. window.Master = video;
  2928. video.src = sound.src;
  2929. video.muted = false;
  2930.  
  2931. // Wait for video to be ready
  2932. await Player.controls.waitForVideoReady();
  2933.  
  2934. if (!isFinite(window.Master.duration)) {
  2935. // Try to estimate from buffered data
  2936. if (window.Master.buffered && window.Master.buffered.length > 0) {
  2937. window.Master.duration = window.Master.buffered.end(window.Master.buffered.length - 1);
  2938. }
  2939. }
  2940.  
  2941. // Start playback
  2942. await video.play();
  2943. }
  2944.  
  2945. // Case 4: just audio
  2946. else if (!sound.hasSoundTag && !sound.isVideo) {
  2947. window.mediaStatus = "Loading"; Player.header.render();
  2948.  
  2949. window.Master = Player.audio;
  2950. Player.audio.src = sound.src;
  2951. image.src = sound.thumb;
  2952.  
  2953. // Wait for Player.audio to be ready
  2954. await Player.controls.waitForAudioReady();
  2955.  
  2956. if (!isFinite(window.Master.duration)) {
  2957. // Try to estimate from buffered data
  2958. if (window.Master.buffered && window.Master.buffered.length > 0) {
  2959. window.Master.duration = window.Master.buffered.end(window.Master.buffered.length - 1);
  2960. }
  2961. }
  2962.  
  2963. // Start playback
  2964. await Player.audio.play();
  2965. }
  2966.  
  2967. //console.log('Master: '+window.Master);
  2968. //console.log('Slave: '+window.Slave);
  2969.  
  2970. // handlePlaybackState
  2971. Player.controls.handlePlaybackState();
  2972.  
  2973. } catch (err) {
  2974. console.error('[8chan sounds player] Playback error:', err);
  2975. Player.logError('Could not play sound');
  2976. window.mediaStatus = "Error"; Player.header.render();
  2977.  
  2978. // Full cleanup
  2979. Player.audio.pause();
  2980. Player.audio.removeAttribute('src');
  2981. Player.audio.load();
  2982. const video = document.querySelector(`.${ns}-video`);
  2983. if (video) {
  2984. video.pause();
  2985. video.removeAttribute('src');
  2986. video.load();
  2987. }
  2988. window.Master = undefined;
  2989. window.Slave = undefined;
  2990.  
  2991. if (syncInterval) clearInterval(syncInterval);
  2992.  
  2993. // handlePlaybackState
  2994. Player.controls.handlePlaybackState();
  2995.  
  2996. return Player.next(); // Skip to next track on error
  2997.  
  2998. } finally {
  2999. window.isLoading = false;
  3000. if(window.mediaStatus !== "Error") { window.mediaStatus = undefined; Player.header.render(); }
  3001. Player.controls.updatePlayButtonState();
  3002. Player.minimised.updatePipSize();
  3003. }
  3004. },
  3005.  
  3006. /**
  3007. * Pause playback.
  3008. */
  3009. pause: function() {
  3010. const video = document.querySelector(`.${ns}-video`);
  3011. if (window.Master !== undefined) window.Master.pause();
  3012. if (video) video.pause();
  3013. Player.controls.handlePlaybackState();
  3014. },
  3015. /**
  3016. * Play the next sound.
  3017. */
  3018. next: function(force = true) {
  3019. Player.controls._movePlaying(1, force);
  3020. },
  3021.  
  3022. /**
  3023. * Play the previous sound.
  3024. */
  3025. previous: function(force = true) {
  3026. Player.controls._movePlaying(-1, force);
  3027. },
  3028.  
  3029. _movePlaying: function(direction, force) {
  3030. if (!Player.audio) return;
  3031. if (window.Master === undefined) return;
  3032. if (!window.Master.ended && !force) return;
  3033.  
  3034. try {
  3035. // If there's no sound fall out.
  3036. if (!Player.sounds.length) return;
  3037. // If there's no sound currently playing or it's not in the list then just play the first sound.
  3038. const currentIndex = Player.sounds.indexOf(Player.playing);
  3039. if (currentIndex === -1) return Player.play(Player.sounds[0]);
  3040.  
  3041. // Calculate next index based on repeat mode
  3042. let nextIndex;
  3043. if (!force && Player.config.repeat === 'one') return; //let loop handle it
  3044. if (!force && Player.config.repeat === 'none') {
  3045. const video = document.querySelector(`.${ns}-video`);
  3046. Player.pause();
  3047. if (video) video.pause();
  3048. return;
  3049. }
  3050. nextIndex = currentIndex + direction;
  3051. // Handle if (Player.config.repeat === 'all') / Wrap around for 'all' mode
  3052. if (nextIndex >= Player.sounds.length) nextIndex = 0;
  3053. if (nextIndex < 0) nextIndex = Player.sounds.length - 1;
  3054.  
  3055. const nextSound = Player.sounds[nextIndex];
  3056. nextSound && Player.play(nextSound);
  3057.  
  3058. Player.set('showSoundTagOnly', false);
  3059. Player.playlist.applySoundTagFilter();
  3060. } catch (err) {
  3061. Player.logError(`There was an error selecting the ${direction > 0 ? 'next' : 'previous'} track. Please check the console for details.`);
  3062. console.error('[8chan sounds player]', err);
  3063. }
  3064. },
  3065.  
  3066. getCurrentPlaybackPosition: function() {
  3067. if (window.Master === undefined) return;
  3068.  
  3069. const video = document.querySelector(`.${ns}-video`);
  3070. return window.Master ? window.Master.currentTime : 0;
  3071. },
  3072.  
  3073. syncPlayback: async function() {
  3074. if (!Player.playing) return;
  3075. if (window.Master === undefined || window.Slave === undefined) return;
  3076. const video = document.querySelector(`.${ns}-video`);
  3077. if (!isFinite(window.Master.duration)) {
  3078. if (syncInterval) clearInterval(syncInterval);
  3079. return;
  3080. }
  3081.  
  3082. // If nothing is playing or Master isn't available, bail out
  3083. if (!window.Master || window.Master.paused) return;
  3084.  
  3085. // if durations don't equal ±2 seconds difference.
  3086. if (window.Slave && (Math.abs(window.Master.duration - window.Slave.duration) > 2)) {
  3087. if (syncInterval) clearInterval(syncInterval);
  3088. video.loop = true;
  3089. Player.audio.loop = Player.config.repeat === 'one';
  3090. return;
  3091. }
  3092.  
  3093. // Sync Slave to Master if it exists and isn't already in sync
  3094. if (window.Slave && (Math.abs(window.Slave.currentTime - window.Master.currentTime) > 0.8)) {
  3095. window.Slave.currentTime = window.Master.currentTime;
  3096. }
  3097. },
  3098.  
  3099. handlePlaybackState: function() {
  3100. const video = document.querySelector(`.${ns}-video`);
  3101. const isPlaying = !Player.audio.paused || (video && !video.paused);
  3102.  
  3103. // Update all play buttons
  3104. document.querySelectorAll(`.${ns}-play-button .${ns}-play-button-display`).forEach(el => {
  3105. el.classList.toggle(`${ns}-play`, !isPlaying);
  3106. });
  3107.  
  3108. // Update container state if needed
  3109. if (Player.container) {
  3110. Player.container.classList.toggle(`${ns}-playing`, isPlaying);
  3111. Player.container.classList.toggle(`${ns}-paused`, !isPlaying);
  3112. }
  3113.  
  3114. Player.controls.updateDuration();
  3115. },
  3116.  
  3117. handleSoundEnded: function() {
  3118. Player.next(false);
  3119. },
  3120. /**
  3121. * Handle sound errors
  3122. */
  3123. handleSoundError: function() {
  3124. const video = document.querySelector(`.${ns}-video`);
  3125.  
  3126. // Clean up blob URLs on error
  3127. if (Player.audio.src && Player.audio.src.startsWith('blob:')) {
  3128. URL.revokeObjectURL(Player.audio.src);
  3129. Player.audio.pause();
  3130. Player.audio.removeAttribute('src');
  3131. Player.audio.load();
  3132. }
  3133.  
  3134. if ((window.Master === video) && video?.error) {
  3135. console.error('[8chan sounds player] Video error:', video.error);
  3136. Player.logError('Video playback error.');
  3137. window.mediaStatus = "Error"; Player.header.render();
  3138. } else if (Player.audio?.error) {
  3139. console.error('[8chan sounds player] Audio error:', Player.audio.error);
  3140. Player.logError('Audio playback error.');
  3141. window.mediaStatus = "Error"; Player.header.render();
  3142. }
  3143. },
  3144. /**
  3145. * Poll for how much has loaded. I know there's the progress event but it unreliable.
  3146. */
  3147. pollForLoading: function() {
  3148. Player._loadingPoll = Player._loadingPoll || setInterval(Player.controls.updateLoaded, 1000);
  3149. },
  3150.  
  3151. /**
  3152. * Stop polling for how much has loaded.
  3153. */
  3154. stopPollingForLoading: function() {
  3155. Player._loadingPoll && clearInterval(Player._loadingPoll);
  3156. Player._loadingPoll = null;
  3157. },
  3158.  
  3159. /**
  3160. * Update the loading bar.
  3161. */
  3162. updateLoaded: function() {
  3163. if (window.Master === undefined) return;
  3164.  
  3165. const video = document.querySelector(`.${ns}-video`);
  3166.  
  3167. let duration = window.Master.duration;
  3168. if (!isFinite(duration)) {
  3169. // Try to estimate from buffered data
  3170. if (window.Master.buffered && window.Master.buffered.length > 0) {
  3171. duration = window.Master.buffered.end(window.Master.buffered.length - 1);
  3172. }
  3173. }
  3174.  
  3175. if (!window.Master || !window.Master.buffered || window.window.Master.buffered.length === 0) return;
  3176.  
  3177. const length = window.Master.buffered.length;
  3178. const size = (window.Master.buffered.end(length - 1) / duration) * 100;
  3179.  
  3180. if (size === 100) {
  3181. Player.controls.stopPollingForLoading();
  3182. }
  3183.  
  3184. if (Player.ui.loadedBar) {
  3185. Player.ui.loadedBar.style.width = size + '%';
  3186. }
  3187. },
  3188.  
  3189.  
  3190. /**
  3191. * Update the seek bar and the duration labels.
  3192. */
  3193. updateDuration: function() {
  3194. if (!Player.container) return;
  3195. if (window.Master === undefined) return;
  3196.  
  3197. const video = document.querySelector(`.${ns}-video`);
  3198.  
  3199. let duration = window.Master.duration;
  3200. if (!isFinite(duration)) {
  3201. // Try to estimate from buffered data
  3202. if (window.Master.buffered && window.Master.buffered.length > 0) {
  3203. duration = window.Master.buffered.end(window.Master.buffered.length - 1);
  3204. }
  3205. }
  3206.  
  3207. const currentTime = Player.controls.getCurrentPlaybackPosition();
  3208.  
  3209. document.querySelectorAll(`.${ns}-current-time`).forEach(el => el.innerHTML = toDuration(currentTime));
  3210. document.querySelectorAll(`.${ns}-duration`).forEach(el => el.innerHTML = '/'+toDuration(duration));
  3211.  
  3212. Player.controls.updateProgressBarPosition(`.${ns}-seek-bar`, Player.ui.currentTimeBar, currentTime, duration);
  3213. },
  3214.  
  3215. /**
  3216. * Update the volume bar.
  3217. */
  3218. updateVolume: function() {
  3219. Player.controls.updateProgressBarPosition(`.${ns}-volume-bar`, Player.$(`.${ns}-volume-bar .${ns}-current-bar`), Player.audio.volume, 1);
  3220. },
  3221.  
  3222. /**
  3223. * Update a progress bar width. Adjust the margin of the circle so it's contained within the bar at both ends.
  3224. */
  3225. updateProgressBarPosition: function(id, bar, current, total) {
  3226. current || (current = 0);
  3227. total || (total = 0);
  3228. const ratio = !total ? 0 : Math.max(0, Math.min(((current || 0) / total), 1));
  3229. bar.style.width = (ratio * 100) + '%';
  3230. if (progressBarStyleSheets[id]) {
  3231. progressBarStyleSheets[id].innerHTML = `${id} .${ns}-current-bar:after {
  3232. margin-right: ${-0.8 * (1 - ratio)}rem;
  3233. }`;
  3234. }
  3235. },
  3236.  
  3237. /**
  3238. * Handle the user interacting with the seek bar.
  3239. */
  3240. handleSeek: function(e) {
  3241. e.preventDefault();
  3242. if (!Player.playing) return;
  3243. if (Player.playing.playing == false) return;
  3244. if (window.Master === undefined) return;
  3245.  
  3246. const video = document.querySelector(`.${ns}-video`);
  3247.  
  3248. let duration = window.Master.duration;
  3249. if (!isFinite(duration)) {
  3250. // Try to estimate from buffered data
  3251. if (window.Master.buffered && window.Master.buffered.length > 0) {
  3252. duration = window.Master.buffered.end(window.Master.buffered.length - 1);
  3253. }
  3254. }
  3255.  
  3256. if (!window.Master || !isFinite(duration)) return;
  3257.  
  3258. const ratio = e.offsetX / parseInt(document.defaultView.getComputedStyle(e.eventTarget || e.target).width, 10);
  3259. const seekTime = duration * ratio;
  3260.  
  3261. // Update media elements
  3262. window.Master.currentTime = seekTime;
  3263. if (Player.playing?.hasSoundTag) {
  3264. if (video) video.currentTime = seekTime;
  3265. }
  3266.  
  3267. if (!window.Master.paused) {
  3268. window.Master.play();
  3269. video.play().catch(() => {});
  3270. }
  3271. Player.controls.handlePlaybackState(); // Resync UI
  3272. },
  3273.  
  3274.  
  3275. /**
  3276. * Handle the user interacting with the volume bar.
  3277. */
  3278. handleVolume: function(e) {
  3279. e.preventDefault();
  3280. if (!Player.container) return;
  3281.  
  3282. const ratio = e.offsetX / parseInt(document.defaultView.getComputedStyle(e.eventTarget || e.target).width, 10);
  3283.  
  3284. Player.audio.volume = Math.max(0, Math.min(ratio, 1));
  3285. const video = document.querySelector(`.${ns}-video`);
  3286. if (video) {
  3287. video.volume = Player.audio.volume;
  3288. }
  3289.  
  3290. // Set the volume value so it can be used for the next session and restore the volume value during the initialization.
  3291. Player.set('volumeValue', Player.audio.volume.toString());
  3292.  
  3293. Player.controls.updateVolume();
  3294. },
  3295. };
  3296. }),
  3297. /* 7 - Display Management
  3298. • Player UI lifecycle:
  3299. o render(): Creates player DOM
  3300. o show()/hide(): Visibility control
  3301. o toggleFullScreen()
  3302. • Handles:
  3303. o 4chan X integration
  3304. o View style switching
  3305. o Drag-and-drop for files
  3306. */
  3307. (function(module, exports) {
  3308. module.exports = {
  3309. atRoot: ['show', 'hide'],
  3310.  
  3311. delegatedEvents: {
  3312. click: {
  3313. [`.${ns}-close-button`]: 'hide'
  3314. },
  3315. fullscreenchange: {
  3316. [`.${ns}-media-and-controls`]: 'display._handleFullScreenChange'
  3317. },
  3318. drop: {
  3319. [`#${ns}-container`]: 'display._handleDrop'
  3320. }
  3321. },
  3322.  
  3323. /**
  3324. * Create the player show/hide button in the 8chan header
  3325. */
  3326. initHeader: function() {
  3327. if (Player.display._initedHeader) {
  3328. return;
  3329. }
  3330.  
  3331. // Find the header navigation container
  3332. const navOptions = document.querySelector('#navOptionsSpan');
  3333. if (!navOptions) {
  3334. return;
  3335. }
  3336.  
  3337. Player.display._initedHeader = true;
  3338.  
  3339. // Create the sounds button
  3340. const soundsButton = createElement(`
  3341. <span>
  3342. <span>/</span>
  3343. <a href="javascript:;" title="8chan Sounds Player" class="coloredIcon" ">
  3344. [♫]
  3345. </a>
  3346. </span>
  3347. `);
  3348.  
  3349. // Insert before the closing bracket
  3350. navOptions.insertBefore(soundsButton, navOptions.lastElementChild);
  3351.  
  3352. // Add click handler
  3353. soundsButton.querySelector('a').addEventListener('click', Player.display.toggle);
  3354.  
  3355. // Also add to mobile menu
  3356. const mobileMenu = document.querySelector('#sidebar-menu ul');
  3357. if (mobileMenu) {
  3358. const mobileItem = createElement(`
  3359. <li>
  3360. <a href="javascript:;" class="coloredIcon">
  3361. Sounds Player
  3362. </a>
  3363. </li>
  3364. `);
  3365. mobileMenu.appendChild(mobileItem);
  3366. mobileItem.querySelector('a').addEventListener('click', Player.display.toggle);
  3367. }
  3368. },
  3369.  
  3370. /**
  3371. * Initialize footer elements
  3372. */
  3373. initFooter: function() {
  3374. if (Player.display._initedFooter) {
  3375. return;
  3376. }
  3377.  
  3378. // Find the footer navigation container
  3379. const threadBottom = document.querySelector('.threadBottom .innerUtility');
  3380. if (!threadBottom) {
  3381. return;
  3382. }
  3383.  
  3384. Player.display._initedFooter = true;
  3385.  
  3386. // Check if sounds link already exists
  3387. if (!threadBottom.querySelector('a[href="javascript:;"][onclick]')) {
  3388. // Create the sounds button
  3389. const soundsButton = createElement(`
  3390. <a href="javascript:;" title="8chan Sounds Player">Sounds Player</a>
  3391. `);
  3392.  
  3393. // Insert after Catalog link
  3394. const catalogLink = threadBottom.querySelector('a[href$="catalog.html"]');
  3395. if (catalogLink) {
  3396. threadBottom.insertBefore(document.createTextNode(' '), catalogLink.nextSibling);
  3397. threadBottom.insertBefore(soundsButton, catalogLink.nextSibling);
  3398. threadBottom.insertBefore(document.createTextNode(' '), catalogLink.nextSibling);
  3399. } else {
  3400. // Fallback if catalog link not found
  3401. threadBottom.insertBefore(document.createTextNode(' '), threadBottom.firstChild);
  3402. threadBottom.insertBefore(soundsButton, threadBottom.firstChild);
  3403. }
  3404.  
  3405. // Add click handler
  3406. soundsButton.addEventListener('click', Player.display.toggle);
  3407. }
  3408. },
  3409.  
  3410. /**
  3411. * Render the player.
  3412. */
  3413. render: async function() {
  3414. try {
  3415. if (Player.container) {
  3416. document.body.removeChild(Player.container);
  3417. document.head.removeChild(Player.stylesheet);
  3418. }
  3419.  
  3420. // Create the main stylesheet.
  3421. Player.display.updateStylesheet();
  3422.  
  3423. // Create the main player. For native threads put it in the threads to get free quote previews.
  3424. const isThread = document.body.classList.contains('is_thread');
  3425. const parent = isThread && !isChanX && document.body.querySelector('.board') || document.body;
  3426. Player.container = createElement(Player.templates.body(), parent);
  3427.  
  3428. Player.trigger('rendered');
  3429. } catch (err) {
  3430. Player.logError('There was an error rendering the sound player. Please check the console for details.');
  3431. console.error('[8chan sounds player]', err);
  3432. // Can't recover, throw.
  3433. throw err;
  3434. }
  3435. },
  3436.  
  3437. updateStylesheet: function() {
  3438. // Insert the stylesheet if it doesn't exist.
  3439. Player.stylesheet = Player.stylesheet || createElement('<style></style>', document.head);
  3440. Player.stylesheet.innerHTML = Player.templates.css();
  3441. },
  3442.  
  3443. /**
  3444. * Change what view is being shown
  3445. */
  3446. setViewStyle: function(style) {
  3447. // Get the size and style prior to switching.
  3448. const previousStyle = Player.config.viewStyle;
  3449. const {
  3450. width,
  3451. height
  3452. } = Player.container.getBoundingClientRect();
  3453.  
  3454. const containerStyle = window.getComputedStyle(document.querySelector(`#${ns}-container`));
  3455. const containerWidth = parseFloat(containerStyle.getPropertyValue('width')) || width;
  3456.  
  3457. // Exit fullscreen before changing to a different view.
  3458. if (style !== 'fullscreen') {
  3459. document.fullscreenElement && document.exitFullscreen();
  3460. }
  3461.  
  3462. // Change the style.
  3463. Player.set('viewStyle', style);
  3464. Player.container.setAttribute('data-view-style', style);
  3465.  
  3466. // Try to reapply the pre change sizing unless it was fullscreen.
  3467. if (previousStyle !== 'fullscreen' || style === 'fullscreen') {
  3468. Player.position.resize(parseInt(containerWidth, 10), parseInt(height, 10));
  3469. }
  3470. Player.trigger('view', style, previousStyle);
  3471. },
  3472.  
  3473. /**
  3474. * Togle the display status of the player.
  3475. */
  3476. toggle: function(e) {
  3477. e && e.preventDefault();
  3478. if (Player.container.style.display === 'none') {
  3479. Player.show();
  3480. } else {
  3481. Player.hide();
  3482. }
  3483. },
  3484.  
  3485. /**
  3486. * Hide the player. Stops polling for changes, and pauses the aduio if set to.
  3487. */
  3488. hide: function(e) {
  3489. if (!Player.container) {
  3490. return;
  3491. }
  3492. try {
  3493. e && e.preventDefault();
  3494. Player.container.style.display = 'none';
  3495.  
  3496. Player.isHidden = true;
  3497. Player.trigger('hide');
  3498. } catch (err) {
  3499. Player.logError('There was an error hiding the sound player. Please check the console for details.');
  3500. console.error('[8chan sounds player]', err);
  3501. }
  3502. },
  3503.  
  3504. /**
  3505. * Show the player. Reapplies the saved position/size, and resumes loaded amount polling if it was paused.
  3506. */
  3507. show: async function(e) {
  3508. if (!Player.container) {
  3509. return;
  3510. }
  3511. try {
  3512. e && e.preventDefault();
  3513. if (!Player.container.style.display) {
  3514. return;
  3515. }
  3516. Player.container.style.display = null;
  3517.  
  3518. Player.isHidden = false;
  3519. await Player.trigger('show');
  3520. } catch (err) {
  3521. Player.logError('There was an error showing the sound player. Please check the console for details.');
  3522. console.error('[8chan sounds player]', err);
  3523. }
  3524. },
  3525.  
  3526. /**
  3527. * Toggle the video/image and controls fullscreen state
  3528. */
  3529. toggleFullScreen: async function() {
  3530. if (!document.fullscreenElement) {
  3531. // Make sure the player (and fullscreen contents) are visible first.
  3532. if (Player.isHidden) {
  3533. Player.show();
  3534. }
  3535. Player.$(`.${ns}-media-and-controls`).requestFullscreen();
  3536. } else if (document.exitFullscreen) {
  3537. document.exitFullscreen();
  3538. }
  3539. },
  3540.  
  3541. /**
  3542. * Handle file/s being dropped on the player.
  3543. */
  3544. _handleDrop: function(e) {
  3545. e.preventDefault();
  3546. e.stopPropagation();
  3547. Player.playlist.addFromFiles(e.dataTransfer.files);
  3548. },
  3549.  
  3550. /**
  3551. * Handle the fullscreen state being changed
  3552. */
  3553. _handleFullScreenChange: function() {
  3554. if (document.fullscreenElement) {
  3555. Player.display.setViewStyle('fullscreen');
  3556. document.querySelector(`.${ns}-image-link`).removeAttribute('href');
  3557. } else {
  3558. if (Player.playing) {
  3559. //document.querySelector(`.${ns}-image-link`).href = Player.playing.image;
  3560. document.querySelector(`.${ns}-image-link`).removeAttribute('href');
  3561. }
  3562. Player.playlist.restore();
  3563. }
  3564. }
  3565. };
  3566. }),
  3567. /* 8 - Event System
  3568. • Custom event bus with:
  3569. o Delegated event handling
  3570. o Audio event bindings
  3571. o Pub/sub pattern (on/off/trigger)
  3572. • Manages all player interactions
  3573. */
  3574. (function(module, exports) {
  3575.  
  3576. module.exports = {
  3577. atRoot: ['on', 'off', 'trigger'],
  3578.  
  3579. // Holder of event handlers.
  3580. _events: {},
  3581. _delegatedEvents: {},
  3582. _undelegatedEvents: {},
  3583. _audioEvents: [],
  3584.  
  3585. initialize: function() {
  3586. const eventLocations = {
  3587. Player,
  3588. ...Player.components
  3589. };
  3590. const delegated = Player.events._delegatedEvents;
  3591. const undelegated = Player.events._undelegatedEvents;
  3592. const audio = Player.events._audioEvents;
  3593.  
  3594. for (let name in eventLocations) {
  3595. const comp = eventLocations[name];
  3596. for (let evt in comp.delegatedEvents || {}) {
  3597. delegated[evt] || (delegated[evt] = []);
  3598. delegated[evt].push(comp.delegatedEvents[evt]);
  3599. }
  3600. for (let evt in comp.undelegatedEvents || {}) {
  3601. undelegated[evt] || (undelegated[evt] = []);
  3602. undelegated[evt].push(comp.undelegatedEvents[evt]);
  3603. }
  3604. comp.audioEvents && (audio.push(comp.audioEvents));
  3605. }
  3606.  
  3607. Player.on('rendered', function() {
  3608. // Wire up delegated events on the container.
  3609. Player.events.addDelegatedListeners(Player.container, delegated);
  3610.  
  3611. // Wire up undelegated events.
  3612. Player.events.addUndelegatedListeners(document, undelegated);
  3613.  
  3614. // Wire up audio events.
  3615. for (let eventList of audio) {
  3616. for (let evt in eventList) {
  3617. Player.audio.addEventListener(evt, Player.events.getHandler(eventList[evt]));
  3618. }
  3619. }
  3620. });
  3621. },
  3622.  
  3623. /**
  3624. * Set delegated events listeners on a target
  3625. */
  3626. addDelegatedListeners(target, events) {
  3627. for (let evt in events) {
  3628. target.addEventListener(evt, function(e) {
  3629. let nodes = [e.target];
  3630. while (nodes[nodes.length - 1] !== target) {
  3631. nodes.push(nodes[nodes.length - 1].parentNode);
  3632. }
  3633. for (let node of nodes) {
  3634. for (let eventList of [].concat(events[evt])) {
  3635. for (let selector in eventList) {
  3636. if (node.matches && node.matches(selector)) {
  3637. e.eventTarget = node;
  3638. let handler = Player.events.getHandler(eventList[selector]);
  3639. // If the handler returns false stop propogation
  3640. if (handler && handler(e) === false) {
  3641. return;
  3642. }
  3643. }
  3644. }
  3645. }
  3646. }
  3647. });
  3648. }
  3649. },
  3650.  
  3651. /**
  3652. * Set, or reset, directly bound events.
  3653. */
  3654. addUndelegatedListeners: function(target, events) {
  3655. for (let evt in events) {
  3656. for (let eventList of [].concat(events[evt])) {
  3657. for (let selector in eventList) {
  3658. target.querySelectorAll(selector).forEach(element => {
  3659. const handler = Player.events.getHandler(eventList[selector]);
  3660. element.removeEventListener(evt, handler);
  3661. element.addEventListener(evt, handler);
  3662. });
  3663. }
  3664. }
  3665. }
  3666. },
  3667.  
  3668. /**
  3669. * Create an event listener on the player.
  3670. *
  3671. * @param {String} evt The name of the events.
  3672. * @param {function} handler The handler function.
  3673. */
  3674. on: function(evt, handler) {
  3675. Player.events._events[evt] || (Player.events._events[evt] = []);
  3676. Player.events._events[evt].push(handler);
  3677. },
  3678.  
  3679. /**
  3680. * Remove an event listener on the player.
  3681. *
  3682. * @param {String} evt The name of the events.
  3683. * @param {function} handler The handler function.
  3684. */
  3685. off: function(evt, handler) {
  3686. const index = Player.events._events[evt] && Player.events._events[evt].indexOf(handler);
  3687. if (index > -1) {
  3688. Player.events._events[evt].splice(index, 1);
  3689. }
  3690. },
  3691.  
  3692. /**
  3693. * Trigger an event on the player.
  3694. *
  3695. * @param {String} evt The name of the events.
  3696. * @param {*} data Data passed to the handler.
  3697. */
  3698. trigger: async function(evt, ...data) {
  3699. const events = Player.events._events[evt] || [];
  3700. for (let handler of events) {
  3701. await handler(...data);
  3702. }
  3703. },
  3704.  
  3705. /**
  3706. * Returns the function of Player referenced by name or a given handler function.
  3707. * @param {String|Function} handler Name to function on Player or a handler function.
  3708. */
  3709. getHandler: function(handler) {
  3710. return typeof handler === 'string' ? _get(Player, handler) : handler;
  3711. }
  3712. };
  3713.  
  3714.  
  3715. }),
  3716. /* 9 - Footer Components
  3717. • Template rendering for:
  3718. o Footer (status info)
  3719. • Uses the user-defined templates
  3720. */
  3721. (function(module, exports) {
  3722.  
  3723. module.exports = {
  3724. initialize: function() {
  3725. Player.userTemplate.maintain(Player.footer, 'footerTemplate');
  3726. },
  3727.  
  3728. render: function() {
  3729. if (Player.container) {
  3730. Player.$(`.${ns}-footer`).innerHTML = Player.templates.footer();
  3731. Player.position.preventWrappingHeaderFooter();
  3732. }
  3733. }
  3734. };
  3735.  
  3736.  
  3737. }),
  3738. /* 10 - Header Components
  3739. • Template rendering for:
  3740. o Player header (controls)
  3741. • Uses the user-defined templates
  3742. */
  3743. (function(module, exports) {
  3744.  
  3745. module.exports = {
  3746. initialize: function() {
  3747. Player.userTemplate.maintain(Player.header, 'headerTemplate');
  3748. },
  3749.  
  3750. render: function() {
  3751. if (Player.container) {
  3752. Player.$(`.${ns}-header`).innerHTML = Player.templates.header();
  3753. }
  3754. }
  3755. };
  3756.  
  3757.  
  3758. }),
  3759. /* 11 - Hotkey System
  3760. • Keyboard control:
  3761. o Binding management
  3762. o Key event handling
  3763. o Modifier key support
  3764. • Configurable activation modes
  3765. */
  3766. (function(module, exports, __webpack_require__) {
  3767.  
  3768. const settingsConfig = __webpack_require__(1);
  3769.  
  3770. module.exports = {
  3771. initialize: function() {
  3772. Player.on('rendered', Player.hotkeys.apply);
  3773. },
  3774.  
  3775. _keyMap: {
  3776. ' ': 'space',
  3777. arrowleft: 'left',
  3778. arrowright: 'right',
  3779. arrowup: 'up',
  3780. arrowdown: 'down'
  3781. },
  3782.  
  3783. addHandler: () => {
  3784. Player.hotkeys.removeHandler();
  3785. document.body.addEventListener('keydown', Player.hotkeys.handle);
  3786. },
  3787. removeHandler: () => {
  3788. document.body.removeEventListener('keydown', Player.hotkeys.handle);
  3789. },
  3790.  
  3791. /**
  3792. * Apply the selecting hotkeys option
  3793. */
  3794. apply: function() {
  3795. const type = Player.config.hotkeys;
  3796. Player.hotkeys.removeHandler();
  3797. Player.off('show', Player.hotkeys.addHandler);
  3798. Player.off('hide', Player.hotkeys.removeHandler);
  3799.  
  3800. if (type === 'always') {
  3801. // If hotkeys are always enabled then just set the handler.
  3802. Player.hotkeys.addHandler();
  3803. } else if (type === 'open') {
  3804. // If hotkeys are only enabled with the player toggle the handler as the player opens/closes.
  3805. // If the player is already open set the handler now.
  3806. if (!Player.isHidden) {
  3807. Player.hotkeys.addHandler();
  3808. }
  3809. Player.on('show', Player.hotkeys.addHandler);
  3810. Player.on('hide', Player.hotkeys.removeHandler);
  3811. }
  3812. },
  3813.  
  3814. /**
  3815. * Handle a keydown even on the body
  3816. */
  3817. handle: function(e) {
  3818. // Ignore events on inputs so you can still type.
  3819. const ignoreFor = ['INPUT', 'SELECT', 'TEXTAREA', 'INPUT'];
  3820. if (ignoreFor.includes(e.target.nodeName) || Player.isHidden && (Player.config.hotkeys !== 'always' || !Player.sounds.length)) {
  3821. return;
  3822. }
  3823. const k = e.key.toLowerCase();
  3824. const bindings = Player.config.hotkey_bindings || {};
  3825.  
  3826. // Look for a matching hotkey binding
  3827. for (let key in bindings) {
  3828. const keyDef = bindings[key];
  3829. const bindingConfig = k === keyDef.key &&
  3830. (!!keyDef.shiftKey === !!e.shiftKey) && (!!keyDef.ctrlKey === !!e.ctrlKey) && (!!keyDef.metaKey === !!e.metaKey) &&
  3831. (!keyDef.ignoreRepeat || !e.repeat) &&
  3832. settingsConfig.find(s => s.property === 'hotkey_bindings').settings.find(s => s.property === 'hotkey_bindings.' + key);
  3833.  
  3834. if (bindingConfig) {
  3835. e.preventDefault();
  3836. return _get(Player, bindingConfig.keyHandler)();
  3837. }
  3838. }
  3839. },
  3840.  
  3841. /**
  3842. * Turn a hotkey definition or key event into an input string.
  3843. */
  3844. stringifyKey: function(key) {
  3845. let k = key.key.toLowerCase();
  3846. Player.hotkeys._keyMap[k] && (k = Player.hotkeys._keyMap[k]);
  3847. return (key.ctrlKey ? 'Ctrl+' : '') + (key.shiftKey ? 'Shift+' : '') + (key.metaKey ? 'Meta+' : '') + k;
  3848. },
  3849.  
  3850. /**
  3851. * Turn an input string into a hotkey definition object.
  3852. */
  3853. parseKey: function(str) {
  3854. const keys = str.split('+');
  3855. let key = keys.pop();
  3856. Object.keys(Player.hotkeys._keyMap).find(k => Player.hotkeys._keyMap[k] === key && (key = k));
  3857. const newValue = {
  3858. key
  3859. };
  3860. keys.forEach(key => newValue[key.toLowerCase() + 'Key'] = true);
  3861. return newValue;
  3862. },
  3863.  
  3864. volumeUp: function() {
  3865. Player.audio.volume = Math.min(Player.audio.volume + 0.05, 1);
  3866. },
  3867.  
  3868. volumeDown: function() {
  3869. Player.audio.volume = Math.max(Player.audio.volume - 0.05, 0);
  3870. }
  3871. };
  3872.  
  3873.  
  3874. }),
  3875. /* 12 - Minimized UI
  3876. • Picture-in-picture mode:
  3877. o Thumbnail display
  3878. o 4chan X header controls
  3879. • Handles compact view states
  3880. */
  3881. (function(module, exports) {
  3882.  
  3883. module.exports = {
  3884. _showingPIP: false,
  3885.  
  3886. initialize: function() {
  3887. if (isChanX) {
  3888. // Create a reply element to gather the style from
  3889. const a = createElement('<a></a>', document.body);
  3890. const style = document.defaultView.getComputedStyle(a);
  3891. createElement(`<style>.${ns}-chan-x-controls .${ns}-media-control > div { background: ${style.color} }</style>`, document.head);
  3892. // Clean up the element.
  3893. document.body.removeChild(a);
  3894.  
  3895. // Set up the contents and maintain user template changes.
  3896. Player.userTemplate.maintain(Player.minimised, 'chanXTemplate', ['chanXControls'], ['show', 'hide']);
  3897. }
  3898. Player.on('rendered', Player.minimised.render);
  3899. Player.on('show', Player.minimised.hidePIP);
  3900. Player.on('hide', Player.minimised.showPIP);
  3901. Player.on('playsound', Player.minimised.showPIP);
  3902. Player.on('config:maxPIPWidth', Player.minimised.updatePipSize);
  3903. Player.on('config:maxPIPHeight', Player.minimised.updatePipSize);
  3904. },
  3905.  
  3906. render: function() {
  3907. if (Player.container && isChanX) {
  3908. let container = document.querySelector(`.${ns}-chan-x-controls`);
  3909. // Create the element if it doesn't exist.
  3910. // Set the user template and control events on it to make all the buttons work.
  3911. if (!container) {
  3912. container = createElementBefore(`<span class="${ns}-chan-x-controls ${ns}-col-auto"></span>`, document.querySelector('#shortcuts').firstElementChild);
  3913. Player.events.addDelegatedListeners(container, {
  3914. click: [Player.userTemplate.delegatedEvents.click, Player.controls.delegatedEvents.click]
  3915. });
  3916. }
  3917.  
  3918. if (Player.config.chanXControls === 'never' || Player.config.chanXControls === 'closed' && !Player.isHidden) {
  3919. return container.innerHTML = '';
  3920. }
  3921.  
  3922. // Render the contents.
  3923. container.innerHTML = Player.userTemplate.build({
  3924. template: Player.config.chanXTemplate,
  3925. sound: Player.playing,
  3926. replacements: {
  3927. 'prev-button': `<div class="${ns}-media-control ${ns}-previous-button"><div class="${ns}-previous-button-display"></div></div>`,
  3928. 'play-button': `<div class="${ns}-media-control ${ns}-play-button"><div class="${ns}-play-button-display ${!Player.audio || Player.audio.paused ? `${ns}-play` : ''}"></div></div>`,
  3929. 'next-button': `<div class="${ns}-media-control ${ns}-next-button"><div class="${ns}-next-button-display"></div></div>`,
  3930. 'sound-current-time': `<span class="${ns}-current-time">0:00</span>`,
  3931. 'sound-duration': `<span class="${ns}-duration">0:00</span>`
  3932. }
  3933. });
  3934. }
  3935. },
  3936.  
  3937. /**
  3938. * Move the image to a picture in picture like thumnail.
  3939. */
  3940. showPIP: function() {
  3941. if (!Player.isHidden || !Player.config.pip || !Player.playing || Player.minimised._showingPIP) {
  3942. return;
  3943. }
  3944. Player.minimised._showingPIP = true;
  3945. const image = document.querySelector(`.${ns}-media`);
  3946. document.body.appendChild(image);
  3947. image.classList.add(`${ns}-pip`);
  3948. image.style.bottom = (Player.position.getHeaderOffset().bottom) + 'px';
  3949.  
  3950. Player.minimised.updatePipSize();
  3951.  
  3952. // Show the player again when the image is clicked.
  3953. image.addEventListener('click', Player.show);
  3954. },
  3955.  
  3956. /**
  3957. * Move the image back to the player.
  3958. */
  3959. hidePIP: function() {
  3960. document.querySelector(`.${ns}-video`).removeAttribute('style');
  3961. document.querySelector(`.${ns}-image`).removeAttribute('style');
  3962. Player.minimised._showingPIP = false;
  3963. const image = document.querySelector(`.${ns}-media`);
  3964. image.style.minWidth = '100%';
  3965. image.style.maxWidth = '100%';
  3966. image.style.maxHeight = '100%';
  3967. Player.$(`.${ns}-media-and-controls`).insertBefore(document.querySelector(`.${ns}-media`), Player.$(`.${ns}-controls`));
  3968. image.classList.remove(`${ns}-pip`);
  3969. image.style.bottom = null;
  3970. image.removeEventListener('click', Player.show);
  3971. },
  3972.  
  3973. updatePipSize: function() {
  3974. const mediaPIP = document.querySelector(`.${ns}-media.${ns}-pip`);
  3975. const videoEl = document.querySelector(`.${ns}-video`);
  3976. const imageEl = document.querySelector(`.${ns}-image`);
  3977.  
  3978. mediaPIP?.removeAttribute('style');
  3979. videoEl?.removeAttribute('style');
  3980. imageEl?.removeAttribute('style');
  3981.  
  3982. if (!Player.isHidden || !Player.config.pip || !Player.playing || !mediaPIP) {
  3983. return;
  3984. }
  3985.  
  3986. const maxWidth = Math.max(1, parseInt(Player.config.maxPIPWidth) || 220); // Fallback: 220px
  3987. const maxHeight = Math.max(1, parseInt(Player.config.maxPIPHeight) || 220); // Fallback: 220px
  3988. // Safely get video dimensions (fallback to container size if missing)
  3989. const videoWidth = Math.max(1, Player.playing?.width || maxWidth);
  3990. const videoHeight = Math.max(1, Player.playing?.height || maxHeight);
  3991. const videoAspectRatio = videoWidth / videoHeight;
  3992. const maxAspectRatio = maxWidth / maxHeight;
  3993.  
  3994. const applyPipDimensions = (element) => {
  3995. let newWidth, newHeight;
  3996.  
  3997. // Determine scaling factor based on aspect ratio comparison
  3998. if (!isFinite(videoAspectRatio) || !isFinite(maxAspectRatio)) {
  3999. // Fallback if invalid aspect ratios
  4000. newWidth = maxWidth;
  4001. newHeight = maxHeight;
  4002. } else if (videoAspectRatio > maxAspectRatio) {
  4003. // Video is wider than container -> scale by width
  4004. newWidth = maxWidth;
  4005. newHeight = maxWidth / videoAspectRatio;
  4006. } else {
  4007. // Video is taller than container -> scale by height
  4008. newHeight = maxHeight;
  4009. newWidth = maxHeight * videoAspectRatio;
  4010. }
  4011.  
  4012. element.style.maxWidth = `${newWidth}px`;
  4013. element.style.maxHeight = `${newHeight}px`;
  4014. mediaPIP.style.maxWidth = `${newWidth}px`;
  4015. mediaPIP.style.maxHeight = `${newHeight}px`;
  4016. };
  4017.  
  4018. if (window.Master === videoEl || window.Master === Player.audio) {
  4019. applyPipDimensions(videoEl);
  4020. applyPipDimensions(imageEl);
  4021. } else {
  4022. mediaPIP.style.width = Player.config.maxPIPWidth;
  4023. mediaPIP.style.height = Player.config.maxPIPHeight;
  4024. }
  4025. }
  4026. };
  4027.  
  4028.  
  4029. }),
  4030. /* 13 - Playlist & Gallery Management
  4031. • Sound collection:
  4032. o add()/remove()
  4033. o Drag-and-drop reordering
  4034. o Filtering
  4035. • Features:
  4036. o Hover image previews
  4037. o Video detection
  4038. o Playlist navigation
  4039. o Gallery thumbnail view
  4040. */
  4041. (function(module, exports, __webpack_require__) {
  4042. const videoFileExtRE = /\.(webm|mp4|m4v|ogv|avi|mpeg|mpg|mpe|m1v|m2v|mov|wmv)$/i;
  4043. const videoMimeRE = /^video\/.+$/;
  4044.  
  4045. const {
  4046. parseFiles,
  4047. parseFileName
  4048. } = __webpack_require__(0);
  4049.  
  4050. module.exports = {
  4051. atRoot: ['add', 'remove'],
  4052.  
  4053. delegatedEvents: {
  4054. click: {
  4055. [`.${ns}-list-item`]: 'playlist.handleSelect',
  4056. [`.${ns}-gallery-item`]: 'playlist.handleSelect',
  4057. [`.${ns}-sound-tag-toggle-button`]: 'playlist.toggleSoundTagPosts'
  4058. },
  4059. mousemove: {
  4060. [`.${ns}-list-item`]: 'playlist.positionHoverImage',
  4061. [`.${ns}-gallery-item`]: 'playlist.positionHoverImage'
  4062. },
  4063. dragstart: {
  4064. [`.${ns}-list-item`]: 'playlist.handleDragStart',
  4065. [`.${ns}-gallery-item`]: 'playlist.handleDragStart'
  4066. },
  4067. dragenter: {
  4068. [`.${ns}-list-item`]: 'playlist.handleDragEnter',
  4069. [`.${ns}-gallery-item`]: 'playlist.handleGalleryDragEnter'
  4070. },
  4071. dragend: {
  4072. [`.${ns}-list-item`]: 'playlist.handleDragEnd',
  4073. [`.${ns}-gallery-item`]: 'playlist.handleDragEnd'
  4074. },
  4075. dragover: {
  4076. [`.${ns}-list-item`]: e => e.preventDefault(),
  4077. [`.${ns}-gallery-item`]: e => e.preventDefault()
  4078. },
  4079. drop: {
  4080. [`.${ns}-list-item`]: e => e.preventDefault(),
  4081. [`.${ns}-gallery-item`]: e => e.preventDefault()
  4082. }
  4083. },
  4084.  
  4085. undelegatedEvents: {
  4086. mouseenter: {
  4087. [`.${ns}-list-item`]: 'playlist.updateHoverImage',
  4088. [`.${ns}-gallery-item`]: 'playlist.updateHoverImage'
  4089. },
  4090. mouseleave: {
  4091. [`.${ns}-list-item`]: 'playlist.removeHoverImage',
  4092. [`.${ns}-gallery-item`]: 'playlist.removeHoverImage'
  4093. }
  4094. },
  4095.  
  4096. initialize: function() {
  4097. // Keep track of the last view style so we can return to it.
  4098. Player.playlist._lastView = Player.config.viewStyle === 'playlist' || Player.config.viewStyle === 'image' || Player.config.viewStyle === 'gallery'
  4099. ? Player.config.viewStyle
  4100. : 'playlist';
  4101.  
  4102. Player.on('view', style => {
  4103. // Focus the playing song when switching views
  4104. if (style === 'playlist') {
  4105. Player.playlist.scrollToPlayingPlaylist();
  4106. } else if (style === 'gallery') {
  4107. Player.playlist.scrollToPlayingGallery();
  4108. }
  4109. // Track state
  4110. if (style === 'playlist' || style === 'image' || style === 'gallery') {
  4111. Player.playlist._lastView = style;
  4112. }
  4113. Player.playlist.setHoverImageVisibility();
  4114. });
  4115.  
  4116. // Update the UI when a new sound plays, and scroll to it
  4117. Player.on('playsound', sound => {
  4118. Player.playlist.showImage(sound);
  4119. Player.$all(`.${ns}-list-item.playing`).forEach(el => el.classList.remove('playing'));
  4120. Player.$all(`.${ns}-gallery-item.playing`).forEach(el => el.classList.remove('playing'));
  4121. Player.$(`.${ns}-list-item[data-id="${Player.playing.id}"]`)?.classList.add('playing');
  4122. Player.$(`.${ns}-gallery-item[data-id="${Player.playing.id}"]`)?.classList.add('playing');
  4123.  
  4124. Player.playlist.scrollToPlaying();
  4125. });
  4126.  
  4127. // Listen to anything that can affect the display
  4128. Player.on('config:filters', Player.playlist.applyFilters);
  4129. Player.on('config:hoverImages', Player.playlist.setHoverImageVisibility);
  4130. Player.on('menu-open', Player.playlist.setHoverImageVisibility);
  4131. Player.on('menu-close', Player.playlist.setHoverImageVisibility);
  4132. Player.on('config:showSoundTagOnly', Player.playlist.applySoundTagFilter);
  4133. },
  4134.  
  4135. /**
  4136. * Render the playlist or gallery based on current view
  4137. */
  4138. render: function() {
  4139. if (!Player.container) {
  4140. return;
  4141. }
  4142.  
  4143. const galleryContainer = Player.$(`.${ns}-gallery-container`);
  4144. galleryContainer.innerHTML = Player.templates.galleryList(); /* module 31 */
  4145.  
  4146. const playlistContainer = Player.$(`.${ns}-list-container`);
  4147. playlistContainer.innerHTML = Player.templates.list(); /* module 25 */
  4148.  
  4149. Player.events.addUndelegatedListeners(document.body, Player.playlist.undelegatedEvents);
  4150. Player.playlist.hoverImage = Player.$(`.${ns}-hover-image`);
  4151. Player.playlist.applySoundTagFilter();
  4152. },
  4153.  
  4154. /**
  4155. * Restore the last playlist or image view.
  4156. */
  4157. restore: function() {
  4158. Player.display.setViewStyle(Player.playlist._lastView || 'playlist');
  4159. },
  4160.  
  4161. /**
  4162. * Update the image displayed in the player.
  4163. */
  4164. showImage: function(sound, thumb) {
  4165. if (!Player.container) {
  4166. return;
  4167. }
  4168. let isVideo = Player.playlist.isVideo = !thumb && (videoFileExtRE.test(sound.image) || videoMimeRE.test(sound.type));
  4169. try {
  4170. const container = document.querySelector(`.${ns}-media`);
  4171. const img = container.querySelector(`.${ns}-image`);
  4172. const video = container.querySelector(`.${ns}-video`);
  4173. img.src = '';
  4174. img.src = isVideo || thumb ? sound.thumb : sound.image;
  4175. video.src = isVideo ? sound.image : undefined;
  4176. container.classList[isVideo ? 'add' : 'remove'](ns + '-show-video');
  4177. } catch (err) {
  4178. Player.logError('There was an error display the sound player image. Please check the console for details.');
  4179. console.error('[8chan sounds player]', err);
  4180. }
  4181. },
  4182.  
  4183. /**
  4184. * Switch between playlist and image view.
  4185. */
  4186. toggleView: function(e) {
  4187. if (!Player.container) {
  4188. return;
  4189. }
  4190. e && e.preventDefault();
  4191. //let style = Player.config.viewStyle === 'playlist' ? 'gallery' : Player.config.viewStyle === 'gallery' ? 'image' : 'playlist';
  4192. let style = Player.config.viewStyle === 'playlist' ? 'gallery' : 'playlist';
  4193. try {
  4194. Player.display.setViewStyle(style);
  4195. //Player.set('viewStyle', style);
  4196. Player.set('config:viewStyle', style);
  4197. //Player.settings.viewStyle = style;
  4198. //Player.container.setAttribute('data-view-style', style);
  4199. Player.playlist.setHoverImageVisibility();
  4200. } catch (err) {
  4201. Player.logError('There was an error switching the view style. Please check the console for details.', 'warning');
  4202. console.error('[8chan sounds player]', err);
  4203. }
  4204. },
  4205.  
  4206. /**
  4207. * Add a new sound from the thread to the player.
  4208. */
  4209. add: function(sound, skipRender) {
  4210. try {
  4211. const id = sound.id;
  4212. // Make sure the sound is not a duplicate.
  4213. if (Player.sounds.find(sound => sound.id === id)) {
  4214. return;
  4215. }
  4216.  
  4217. // Add the sound with the location based on the shuffle settings.
  4218. let index = Player.config.shuffle ?
  4219. Math.floor(Math.random() * Player.sounds.length - 1) :
  4220. Player.sounds.findIndex(s => Player.compareIds(s.id, id) > 1);
  4221. index < 0 && (index = Player.sounds.length);
  4222. Player.sounds.splice(index, 0, sound);
  4223.  
  4224. if (Player.container) {
  4225. if (!skipRender) {
  4226.  
  4227. // Add the sound to the gallery.
  4228. const galleryContainer = Player.$(`.${ns}-gallery-container`);
  4229. let itemContainer = document.createElement('div');
  4230. itemContainer.innerHTML = Player.templates.galleryList({
  4231. sounds: [sound]
  4232. });
  4233. Player.events.addUndelegatedListeners(itemContainer, Player.playlist.undelegatedEvents);
  4234. let item = itemContainer.children[0];
  4235. if (index < Player.sounds.length - 1) {
  4236. const before = Player.$(`.${ns}-gallery-item[data-id="${Player.sounds[index + 1].id}"]`);
  4237. galleryContainer.insertBefore(item, before);
  4238. } else {
  4239. galleryContainer.appendChild(item);
  4240. }
  4241.  
  4242. // Add the sound to the playlist.
  4243. const list = Player.$(`.${ns}-list-container`);
  4244. let rowContainer = document.createElement('div');
  4245. rowContainer.innerHTML = Player.templates.list({
  4246. sounds: [sound]
  4247. });
  4248. Player.events.addUndelegatedListeners(rowContainer, Player.playlist.undelegatedEvents);
  4249. let row = rowContainer.children[0];
  4250. if (index < Player.sounds.length - 1) {
  4251. const before = Player.$(`.${ns}-list-item[data-id="${Player.sounds[index + 1].id}"]`);
  4252. list.insertBefore(row, before);
  4253. } else {
  4254. list.appendChild(row);
  4255. }
  4256.  
  4257. }
  4258.  
  4259. // If nothing else has been added yet show the image for this sound.
  4260. if (Player.sounds.length === 1) {
  4261. // If we're on a thread with autoshow enabled then make sure the player is displayed
  4262. if (/\/thread\//.test(location.href) && Player.config.autoshow) {
  4263. Player.show();
  4264. }
  4265. Player.playlist.showImage(sound);
  4266. }
  4267. Player.trigger('add', sound);
  4268.  
  4269. Player.playlist.applySoundTagFilter(); // filter new sounds
  4270. }
  4271. } catch (err) {
  4272. Player.logError('There was an error adding to the sound player. Please check the console for details.');
  4273. console.log('[8chan sounds player]', sound);
  4274. console.error('[8chan sounds player]', err);
  4275. }
  4276. },
  4277.  
  4278. /**
  4279. * Add a new local sound from the users computer to the player.
  4280. */
  4281. addFromFiles: async function(files) {
  4282. for (const file of files) {
  4283. // Skip non-media files
  4284. if (!file.type.startsWith('image') && !file.type.startsWith('video/')) {
  4285. console.warn("[8chan sounds player] localFile is not an image or video");
  4286. return;
  4287. }
  4288.  
  4289. const filenameRE = /(.*?)[[({](?:audio|sound)[ =:|$](.*?)[\])}]/gi;
  4290. const filenameRE2 = /(\[([^\]]*(?:catbox\.moe)[^\]]*)\])/gi;
  4291. if (file.type.startsWith('image')
  4292. && (!filenameRE.test(file.name) || (!filenameRE2.test(file.name)))) {
  4293. console.warn("[8chan sounds player] localFile: image without [sound=URL]");
  4294. return;
  4295. }
  4296.  
  4297. try {
  4298. // Convert file to base64 data URL instead of blob URL
  4299. const dataUrl = await new Promise((resolve) => {
  4300. const reader = new FileReader();
  4301. reader.onload = () => resolve(reader.result);
  4302. reader.readAsDataURL(file);
  4303. });
  4304.  
  4305. const videoFileExtRE = /(webm|mp4|m4v|ogv|avi|mpeg|mpg|mpe|m1v|m2v|mov|wmv)$/i;
  4306.  
  4307. const imageSrc = dataUrl;
  4308. const type = file.type;
  4309. let thumbSrc = imageSrc;
  4310. const fileURL = dataUrl;
  4311. const fileExt = file.name.split('.').pop().toLowerCase();
  4312. const isVideo = videoFileExtRE.test(fileExt);
  4313.  
  4314. if (isVideo) {
  4315. // Create video thumbnail
  4316. const videoTmp = document.createElement('video');
  4317. const canvas = document.createElement('canvas');
  4318. const ctx = canvas.getContext('2d');
  4319.  
  4320. await new Promise((resolve) => {
  4321. videoTmp.addEventListener('loadeddata', () => {
  4322. // setTimeout to avoid black thumbnails
  4323. setTimeout(() => {
  4324. canvas.width = videoTmp.videoWidth;
  4325. canvas.height = videoTmp.videoHeight;
  4326. ctx.drawImage(videoTmp, 0, 0, canvas.width, canvas.height);
  4327. thumbSrc = canvas.toDataURL('image/jpeg');
  4328. resolve();
  4329. }, 100);
  4330. });
  4331. videoTmp.src = dataUrl;
  4332. videoTmp.currentTime = 0.1; // Seek to a small time to get a frame
  4333. });
  4334. }
  4335.  
  4336. function formatFileSize(bytes) {
  4337. if (bytes === 0) return '0 KB';
  4338.  
  4339. const units = ['KB', 'MB', 'GB'];
  4340. const i = Math.floor(Math.log(bytes) / Math.log(1024));
  4341.  
  4342. // Ensure we never return "Bytes" (always at least KB)
  4343. const adjustedSize = i === 0 ? bytes / 1024 : bytes / Math.pow(1024, i);
  4344. const unit = i === 0 ? 'KB' : units[i - 1];
  4345.  
  4346. return adjustedSize.toFixed(2) + ' ' + unit;
  4347. }
  4348.  
  4349. const fileSize = formatFileSize(file.size);
  4350.  
  4351. //function parseFileName(filename, imageSrc, postNumber, thumbSrc, imageMD5, fileIndex, fileSize, dataFilemime);
  4352. parseFileName(file.name, imageSrc, 'locF:'+window.localFileCounter, thumbSrc, null, 'lF'+window.localFileCounter, fileSize, file.type)
  4353. .forEach(sound => Player.add({
  4354. ...sound,
  4355. id: 'locF:' + window.localFileCounter,
  4356. local: true,
  4357. type,
  4358. }));
  4359.  
  4360. window.localFileCounter++;
  4361. } catch (error) {
  4362. console.error('[8chan sounds player] Error processing file:', file.name, error);
  4363. }
  4364. }
  4365. },
  4366.  
  4367. /**
  4368. * Remove a sound
  4369. */
  4370. remove: function(sound) {
  4371. const index = Player.sounds.indexOf(sound);
  4372.  
  4373. // If the playing sound is being removed then play the next sound.
  4374. if (Player.playing === sound) {
  4375. Player.pause();
  4376. Player.next(true);
  4377. }
  4378. // Remove the sound from the the list and play order.
  4379. if (index > -1) {
  4380. Player.sounds.splice(index, 1);
  4381.  
  4382. // Clean up blob URLs only for local files
  4383. if (sound.local) {
  4384. if (sound.url?.startsWith('blob:')) URL.revokeObjectURL(sound.url);
  4385. if (sound.image?.startsWith('blob:')) URL.revokeObjectURL(sound.image);
  4386. if (sound.thumb?.startsWith('blob:')) URL.revokeObjectURL(sound.thumb);
  4387. }
  4388. }
  4389. // Remove the item from the list.
  4390. Player.$(`.${ns}-list-container`)?.removeChild(Player.$(`.${ns}-list-item[data-id="${sound.id}"]`));
  4391. Player.$(`.${ns}-gallery-container`)?.removeChild(Player.$(`.${ns}-gallery-item[data-id="${sound.id}"]`));
  4392. Player.trigger('remove', sound);
  4393. },
  4394.  
  4395. /**
  4396. * Handle an playlist/gallery item being clicked. Either open/close the menu or play the sound.
  4397. */
  4398. handleSelect: function(e) {
  4399. // Ignore if a link was clicked.
  4400. if (e.target.nodeName === 'A' || e.target.closest('a')) {
  4401. return;
  4402. }
  4403. e.preventDefault();
  4404. const id = e.eventTarget.getAttribute('data-id');
  4405. const sound = id && Player.sounds.find(sound => sound.id === id);
  4406. sound && Player.play(sound);
  4407. },
  4408.  
  4409. /**
  4410. * Read all the sounds from the thread again.
  4411. */
  4412. refresh: function() {
  4413. parseFiles(document.body);
  4414. },
  4415.  
  4416. /**
  4417. * Toggle the hoverImages setting
  4418. */
  4419. toggleHoverImages: function(e) {
  4420. e && e.preventDefault();
  4421. Player.set('hoverImages', !Player.config.hoverImages);
  4422. },
  4423.  
  4424. /**
  4425. * Only show the hover image with the setting enabled, no item menu open, and nothing being dragged.
  4426. */
  4427. setHoverImageVisibility: function() {
  4428. const container = Player.$(`.${ns}-player`);
  4429. const hideImage = !Player.config.hoverImages ||
  4430. Player.playlist._dragging ||
  4431. container.querySelector(`.${ns}-item-menu`);
  4432. container.classList[hideImage ? 'add' : 'remove'](`${ns}-hide-hover-image`);
  4433. },
  4434.  
  4435. /**
  4436. * Set the displayed hover image and reposition.
  4437. */
  4438. updateHoverImage: function(e) {
  4439. const id = e.currentTarget.getAttribute('data-id');
  4440. const sound = Player.sounds.find(sound => sound.id === id);
  4441. Player.config.viewStyle === 'gallery' ? Player.playlist.hoverImage.style.display = 'none' : Player.playlist.hoverImage.style.display = 'block';
  4442. Player.playlist.hoverImage.setAttribute('src', sound.thumb);
  4443. Player.playlist.positionHoverImage(e);
  4444. },
  4445.  
  4446. /**
  4447. * Reposition the hover image to follow the cursor.
  4448. */
  4449. positionHoverImage: function(e) {
  4450. const {
  4451. width,
  4452. height
  4453. } = Player.playlist.hoverImage.getBoundingClientRect();
  4454. const maxX = document.documentElement.clientWidth - width - 5;
  4455. Player.playlist.hoverImage.style.left = (Math.min(e.clientX, maxX) + 5) + 'px';
  4456. Player.playlist.hoverImage.style.top = (e.clientY - height - 10) + 'px';
  4457. },
  4458.  
  4459. /**
  4460. * Hide the hover image when nothing is being hovered over.
  4461. */
  4462. removeHoverImage: function() {
  4463. Player.playlist.hoverImage.style.display = 'none';
  4464. },
  4465.  
  4466. /**
  4467. * Start dragging a playlist item.
  4468. */
  4469. handleDragStart: function(e) {
  4470. Player.playlist._dragging = e.eventTarget;
  4471. Player.playlist.setHoverImageVisibility();
  4472. e.eventTarget.classList.add(`${ns}-dragging`);
  4473. setTimeout(() => {e.dataTransfer.setDragImage(new Image(), 0, 0);}, 100);
  4474. e.dataTransfer.dropEffect = 'move';
  4475. e.dataTransfer.setData('text/plain', e.eventTarget.getAttribute('data-id'));
  4476. },
  4477.  
  4478. /**
  4479. * Swap a playlist item when it's dragged over another item (and update gallery accordingly)
  4480. */
  4481. handleDragEnter: function(e) {
  4482. if (!Player.playlist._dragging) return;
  4483. e.preventDefault();
  4484.  
  4485. const moving = Player.playlist._dragging;
  4486. const id = moving.getAttribute('data-id');
  4487. let before = e.target.closest(`.${ns}-list-item`);
  4488. if (!before || moving === before) return;
  4489.  
  4490. // Get corresponding gallery elements
  4491. const movingGallery = Player.$(`.${ns}-gallery-item[data-id="${id}"]`);
  4492. const beforeId = before.getAttribute('data-id');
  4493. let beforeGallery = Player.$(`.${ns}-gallery-item[data-id="${beforeId}"]`);
  4494.  
  4495. const movingIdx = Player.sounds.findIndex(s => s.id === id);
  4496. const list = moving.parentNode;
  4497. const galleryList = movingGallery?.parentNode;
  4498.  
  4499. // Calculate position
  4500. const position = moving.compareDocumentPosition(before);
  4501. if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
  4502. before = before.nextElementSibling;
  4503. if (beforeGallery) {
  4504. beforeGallery = beforeGallery.nextElementSibling;
  4505. }
  4506. }
  4507.  
  4508. // Safely move elements
  4509. try {
  4510. // Update playlist view
  4511. if (before && list.contains(before)) {
  4512. list.insertBefore(moving, before);
  4513. } else {
  4514. list.appendChild(moving);
  4515. }
  4516.  
  4517. // Update gallery view if elements exist
  4518. if (movingGallery && galleryList) {
  4519. if (beforeGallery && galleryList.contains(beforeGallery)) {
  4520. galleryList.insertBefore(movingGallery, beforeGallery);
  4521. } else {
  4522. galleryList.appendChild(movingGallery);
  4523. }
  4524. }
  4525.  
  4526. // Update sounds array
  4527. const newIndex = before ?
  4528. Player.sounds.findIndex(s => s.id === before.getAttribute('data-id')) :
  4529. Player.sounds.length;
  4530. const [movedSound] = Player.sounds.splice(movingIdx, 1);
  4531. Player.sounds.splice(newIndex, 0, movedSound);
  4532.  
  4533. Player.trigger('order');
  4534. } catch (err) {
  4535. console.error('[8chan sounds player] Drag operation failed:', err);
  4536. }
  4537. },
  4538.  
  4539. /**
  4540. * Handle gallery item drag over (and update playlist accordingly)
  4541. */
  4542. handleGalleryDragEnter: function(e) {
  4543. if (!Player.playlist._dragging) return;
  4544. e.preventDefault();
  4545.  
  4546. const moving = Player.playlist._dragging;
  4547. const id = moving.getAttribute('data-id');
  4548. let before = e.target.closest(`.${ns}-gallery-item`);
  4549. if (!before || moving === before) return;
  4550.  
  4551. // Get corresponding playlist elements
  4552. const movingPlaylist = Player.$(`.${ns}-list-item[data-id="${id}"]`);
  4553. const beforeId = before.getAttribute('data-id');
  4554. let beforePlaylist = Player.$(`.${ns}-list-item[data-id="${beforeId}"]`);
  4555.  
  4556. const movingIdx = Player.sounds.findIndex(s => s.id === id);
  4557. const list = moving.parentNode;
  4558. const playlistList = movingPlaylist?.parentNode;
  4559.  
  4560. // Calculate position
  4561. const position = moving.compareDocumentPosition(before);
  4562. if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
  4563. before = before.nextElementSibling;
  4564. if (beforePlaylist) {
  4565. beforePlaylist = beforePlaylist.nextElementSibling;
  4566. }
  4567. }
  4568.  
  4569. // Safely move elements
  4570. try {
  4571. // Update gallery view
  4572. if (before && list.contains(before)) {
  4573. list.insertBefore(moving, before);
  4574. } else {
  4575. list.appendChild(moving);
  4576. }
  4577.  
  4578. // Update playlist view if elements exist
  4579. if (movingPlaylist && playlistList) {
  4580. if (beforePlaylist && playlistList.contains(beforePlaylist)) {
  4581. playlistList.insertBefore(movingPlaylist, beforePlaylist);
  4582. } else {
  4583. playlistList.appendChild(movingPlaylist);
  4584. }
  4585. }
  4586.  
  4587. // Update sounds array
  4588. const newIndex = before ?
  4589. Player.sounds.findIndex(s => s.id === before.getAttribute('data-id')) :
  4590. Player.sounds.length;
  4591. const [movedSound] = Player.sounds.splice(movingIdx, 1);
  4592. Player.sounds.splice(newIndex, 0, movedSound);
  4593.  
  4594. Player.trigger('order');
  4595. } catch (err) {
  4596. console.error('[8chan sounds player] Drag operation failed:', err);
  4597. }
  4598. },
  4599.  
  4600. /**
  4601. * Start dragging a playlist item.
  4602. */
  4603. handleDragEnd: function(e) {
  4604. if (!Player.playlist._dragging) {
  4605. return;
  4606. }
  4607. e.preventDefault();
  4608. delete Player.playlist._dragging;
  4609. e.eventTarget.classList.remove(`${ns}-dragging`);
  4610. Player.playlist.setHoverImageVisibility();
  4611. },
  4612.  
  4613. /**
  4614. * Scroll to the playing item.
  4615. */
  4616. scrollToPlaying: function(type = 'center') {
  4617. if (Player.config.viewStyle === 'playlist') {
  4618. Player.playlist.scrollToPlayingPlaylist(type);
  4619. } else if (Player.config.viewStyle === 'gallery') {
  4620. Player.playlist.scrollToPlayingGallery(type);
  4621. }
  4622. },
  4623.  
  4624. /**
  4625. * Scroll to the playing item, unless there is an open menu in the playlist.
  4626. */
  4627. scrollToPlayingPlaylist: function(type = 'center') {
  4628. if (Player.$(`.${ns}-list-container .${ns}-item-menu`)) {
  4629. return;
  4630. }
  4631. const playing = Player.$(`.${ns}-list-item.playing`);
  4632. playing && playing.scrollIntoView({ behavior: 'smooth', block: type });
  4633. },
  4634.  
  4635. /**
  4636. * Scroll to playing item in gallery view
  4637. */
  4638. scrollToPlayingGallery: function(type = 'center') {
  4639. const playing = Player.$(`.${ns}-gallery-item.playing`);
  4640. playing && playing.scrollIntoView({ behavior: 'smooth', block: type });
  4641. },
  4642.  
  4643. /**
  4644. * Remove any user filtered items from the playlist.
  4645. */
  4646. applyFilters: function() {
  4647. Player.sounds.filter(sound => !Player.acceptedSound(sound)).forEach(Player.playlist.remove);
  4648. },
  4649.  
  4650. toggleSoundTagPosts: function(e) {
  4651. e && e.preventDefault();
  4652. Player.set('showSoundTagOnly', !Player.config.showSoundTagOnly);
  4653. Player.playlist.applySoundTagFilter();
  4654. },
  4655.  
  4656. applySoundTagFilter: function() {
  4657. const showSoundTagOnly = Player.config.showSoundTagOnly;
  4658.  
  4659. // Update button text
  4660. const buttons = document.querySelectorAll(`.${ns}-sound-tag-toggle-button`);
  4661. buttons.forEach(button => {
  4662. button.title = showSoundTagOnly ? 'Show all posts' : 'Show only sound posts';
  4663. });
  4664.  
  4665. // Filter playlist items
  4666. const listItems = Player.$all(`.${ns}-list-item`);
  4667. listItems.forEach(item => {
  4668. const id = item.getAttribute('data-id');
  4669. const sound = Player.sounds.find(s => s.id === id);
  4670. if (sound) {
  4671. item.style.display = showSoundTagOnly && !sound.hasSoundTag ? 'none' : '';
  4672. }
  4673. });
  4674.  
  4675. // Filter gallery items
  4676. const galleryItems = Player.$all(`.${ns}-gallery-item`);
  4677. galleryItems.forEach(item => {
  4678. const id = item.getAttribute('data-id');
  4679. const sound = Player.sounds.find(s => s.id === id);
  4680. if (sound) {
  4681. item.style.display = showSoundTagOnly && !sound.hasSoundTag ? 'none' : '';
  4682. }
  4683. });
  4684.  
  4685. // Focus the playing song
  4686. Player.playlist.scrollToPlaying();
  4687. }
  4688. };
  4689. }),
  4690. /* 14 - Positioning
  4691. • Player window:
  4692. o Draggable header
  4693. o Resizable
  4694. o Smart post width limiting
  4695. • Handles:
  4696. o Saved position/size
  4697. o Viewport constraints
  4698. o 4chan X header offsets
  4699. */
  4700. (function(module, exports) {
  4701.  
  4702. module.exports = {
  4703. delegatedEvents: {
  4704. mousedown: {
  4705. [`.${ns}-header`]: 'position.initMove',
  4706. [`.${ns}-expander`]: 'position.initResize'
  4707. }
  4708. },
  4709.  
  4710. initialize: function() {
  4711. // Apply the last position/size, and post width limiting, when the player is shown.
  4712. Player.on('show', async function() {
  4713. const [top, left] = (await GM.getValue('position') || '').split(':');
  4714. const [width, height] = (await GM.getValue('size') || '').split(':'); +
  4715. top && +left && Player.position.move(top, left, true); +
  4716. width && +height && Player.position.resize(width, height);
  4717.  
  4718. // Ensure player is on screen when shown
  4719. Player.position.ensureOnScreen();
  4720.  
  4721. if (Player.config.limitPostWidths) {
  4722. Player.position.setPostWidths();
  4723. window.addEventListener('scroll', Player.position.setPostWidths);
  4724. }
  4725. });
  4726.  
  4727. // Remove post width limiting when the player is hidden.
  4728. Player.on('hide', function() {
  4729. Player.position.setPostWidths();
  4730. window.removeEventListener('scroll', Player.position.setPostWidths);
  4731. });
  4732.  
  4733. // Reapply the post width limiting config values when they're changed.
  4734. Player.on('config', prop => {
  4735. if (prop === 'limitPostWidths' || prop === 'minPostWidth') {
  4736. window.removeEventListener('scroll', Player.position.setPostWidths);
  4737. Player.position.setPostWidths();
  4738. if (Player.config.limitPostWidths) {
  4739. window.addEventListener('scroll', Player.position.setPostWidths);
  4740. }
  4741. }
  4742. });
  4743.  
  4744. // Remove post width limit from inline quotes
  4745. /*new MutationObserver(function() {
  4746. document.querySelectorAll('#hoverUI .postContainer, .inline .postContainer, .backlink_container article').forEach(post => {
  4747. post.style.maxWidth = null;
  4748. post.style.minWidth = null;
  4749. });
  4750. }).observe(document.body, {
  4751. childList: true,
  4752. subtree: true
  4753. });*/
  4754.  
  4755. this.debouncedResize = window.debounceFc(() => {
  4756. if (Player.config.limitPostWidths) {
  4757. Player.position.setPostWidths();
  4758. }
  4759. Player.position.preventWrapping();
  4760. Player.position.preventWrappingHeaderFooter();
  4761. }, 8);
  4762.  
  4763. window.addEventListener('resize', this.debouncedResize);
  4764.  
  4765. // Document resize observer
  4766. this.resizeObserver = new ResizeObserver(entries => {
  4767. if (Player.container && !Player.isHidden) {
  4768. Player.position.ensureOnScreen();
  4769. }
  4770. });
  4771.  
  4772. this.resizeObserver.observe(document.documentElement);
  4773. this.resizeObserver.observe(document.body);
  4774.  
  4775. // Listen for changes from other tabs
  4776. Player.syncTab('position', value => Player.position.move(...value.split(':').concat(true)));
  4777. Player.syncTab('size', value => Player.position.resize(...value.split(':')));
  4778.  
  4779. Player.on("config:preventControlsWrapping", (e) => !e && Player.position.showAllControls());
  4780. Player.on("config:controlsHideOrder", () => {
  4781. Player.position.setHideOrder();
  4782. Player.position.preventWrapping();
  4783. });
  4784. },
  4785.  
  4786. /**
  4787. * Applies a max width to posts next to the player so they don't get hidden behind it.
  4788. */
  4789. setPostWidths: window.throttleFc(function() {
  4790. const offset = (document.documentElement.clientWidth - Player.container.offsetLeft) + 10;
  4791. const selector = '.innerPost';
  4792. const enabled = !Player.isHidden && Player.config.limitPostWidths;
  4793. const startY = Player.container.offsetTop;
  4794. const endY = Player.container.getBoundingClientRect().height + startY;
  4795.  
  4796. document.querySelectorAll(selector).forEach(post => {
  4797. const rect = enabled && post.getBoundingClientRect();
  4798. const limitWidth = enabled && rect.top + rect.height > startY && rect.top < endY;
  4799. post.style.maxWidth = limitWidth ? `calc(100% - ${offset}px)` : null;
  4800. post.style.minWidth = limitWidth && Player.config.minPostWidth ? `${Player.config.minPostWidth}` : null;
  4801. });
  4802. }, 100),
  4803.  
  4804. /**
  4805. * Handle the user grabbing the expander.
  4806. */
  4807. initResize: function initDrag(e) {
  4808. e.preventDefault();
  4809. Player._startX = e.clientX;
  4810. Player._startY = e.clientY;
  4811. let {
  4812. width,
  4813. height
  4814. } = Player.container.getBoundingClientRect();
  4815. Player._startWidth = width;
  4816. Player._startHeight = height;
  4817. document.documentElement.addEventListener('mousemove', Player.position.doResize, false);
  4818. document.documentElement.addEventListener('mouseup', Player.position.stopResize, false);
  4819. },
  4820.  
  4821. /**
  4822. * Handle the user dragging the expander.
  4823. */
  4824. doResize: function(e) {
  4825. e.preventDefault();
  4826. Player.position.resize(Player._startWidth + e.clientX - Player._startX, Player._startHeight + e.clientY - Player._startY);
  4827. },
  4828.  
  4829. /**
  4830. * Handle the user releasing the expander.
  4831. */
  4832. stopResize: function() {
  4833. const {
  4834. width,
  4835. height
  4836. } = Player.container.getBoundingClientRect();
  4837. document.documentElement.removeEventListener('mousemove', Player.position.doResize, false);
  4838. document.documentElement.removeEventListener('mouseup', Player.position.stopResize, false);
  4839. GM.setValue('size', width + ':' + height);
  4840. },
  4841.  
  4842. /**
  4843. * Resize the player.
  4844. */
  4845. resize: function(width, height) {
  4846. if (!Player.container || Player.config.viewStyle === 'fullscreen') {
  4847. return;
  4848. }
  4849. const {
  4850. bottom
  4851. } = Player.position.getHeaderOffset();
  4852. // Make sure the player isn't going off screen.
  4853. height = Math.min(height, document.documentElement.clientHeight - Player.container.offsetTop - bottom);
  4854. width = Math.min(Math.ceil(width), document.documentElement.clientWidth - Player.container.offsetLeft);
  4855.  
  4856. Player.container.style.width = width + 'px';
  4857.  
  4858. // Which element to change the height of depends on the view being displayed.
  4859. const heightElement = Player.config.viewStyle === 'playlist' ? Player.$(`.${ns}-list-container`) :
  4860. Player.config.viewStyle === 'gallery' ? Player.$(`.${ns}-gallery-container`) :
  4861. Player.config.viewStyle === 'image' ? Player.$(`.${ns}-media`) :
  4862. Player.config.viewStyle === 'settings' ? Player.$(`.${ns}-settings`) :
  4863. Player.config.viewStyle === 'threads' ? Player.$(`.${ns}-threads`) : null;
  4864.  
  4865. if (!heightElement) {
  4866. return;
  4867. }
  4868.  
  4869. const offset = Player.container.getBoundingClientRect().height - heightElement.getBoundingClientRect().height;
  4870. heightElement.style.height = (height - offset) + 'px';
  4871.  
  4872. // Check control wrapping after resize
  4873. Player.position.preventWrapping();
  4874. Player.position.preventWrappingHeaderFooter();
  4875. },
  4876.  
  4877. /**
  4878. * Handle the user grabbing the header.
  4879. */
  4880. initMove: function(e) {
  4881. e.preventDefault();
  4882. Player.$(`.${ns}-header`).style.cursor = 'grabbing';
  4883.  
  4884. // Try to reapply the current sizing to fix oversized winows.
  4885. const {
  4886. width,
  4887. height
  4888. } = Player.container.getBoundingClientRect();
  4889.  
  4890. const containerStyle = window.getComputedStyle(document.querySelector(`#${ns}-container`));
  4891. const containerWidth = parseFloat(containerStyle.getPropertyValue('width')) || width;
  4892.  
  4893. Player.position.resize(containerWidth, height);
  4894.  
  4895. Player._offsetX = e.clientX - Player.container.offsetLeft;
  4896. Player._offsetY = e.clientY - Player.container.offsetTop;
  4897. document.documentElement.addEventListener('mousemove', Player.position.doMove, false);
  4898. document.documentElement.addEventListener('mouseup', Player.position.stopMove, false);
  4899. },
  4900.  
  4901. /**
  4902. * Handle the user dragging the header.
  4903. */
  4904. doMove: function(e) {
  4905. e.preventDefault();
  4906. Player.position.move(e.clientX - Player._offsetX, e.clientY - Player._offsetY);
  4907. },
  4908.  
  4909. /**
  4910. * Handle the user releasing the header.
  4911. */
  4912. stopMove: function() {
  4913. document.documentElement.removeEventListener('mousemove', Player.position.doMove, false);
  4914. document.documentElement.removeEventListener('mouseup', Player.position.stopMove, false);
  4915. Player.$(`.${ns}-header`).style.cursor = null;
  4916. GM.setValue('position', parseInt(Player.container.style.left, 10) + ':' + parseInt(Player.container.style.top, 10));
  4917. },
  4918.  
  4919. /**
  4920. * Move the player.
  4921. */
  4922. move: function(x, y, allowOffscreen) {
  4923. if (!Player.container) {
  4924. return;
  4925. }
  4926.  
  4927. const {
  4928. top,
  4929. bottom
  4930. } = Player.position.getHeaderOffset();
  4931.  
  4932. // Ensure the player stays fully within the window.
  4933. const {
  4934. width,
  4935. height
  4936. } = Player.container.getBoundingClientRect();
  4937. const maxX = allowOffscreen ? Infinity : document.documentElement.clientWidth - width;
  4938. const maxY = allowOffscreen ? Infinity : document.documentElement.clientHeight - height - bottom;
  4939.  
  4940. // Move the window.
  4941. Player.container.style.left = Math.max(0, Math.min(x, maxX)) + 'px';
  4942. Player.container.style.top = Math.max(top, Math.min(y, maxY)) + 'px';
  4943.  
  4944. if (Player.config.limitPostWidths) {
  4945. Player.position.setPostWidths();
  4946. }
  4947. },
  4948.  
  4949. /**
  4950. * Get the offset from the top or bottom required for the 4chan X header.
  4951. */
  4952. getHeaderOffset: function() {
  4953. const top = 26;
  4954. const bottom = 0;
  4955.  
  4956. return {
  4957. top,
  4958. bottom
  4959. };
  4960. },
  4961.  
  4962. /**
  4963. * Ensures the player is within the visible screen area
  4964. */
  4965. ensureOnScreen: function() {
  4966. if (!Player.container || Player.isHidden || Player.config.viewStyle === 'fullscreen') {
  4967. return;
  4968. }
  4969.  
  4970. const containerRect = Player.container.getBoundingClientRect();
  4971. const viewportWidth = document.documentElement.clientWidth;
  4972. const viewportHeight = document.documentElement.clientHeight;
  4973. const { top: headerTop, bottom: headerBottom } = this.getHeaderOffset();
  4974.  
  4975. // Check if player is completely offscreen
  4976. const isOffscreen =
  4977. containerRect.right < 0 ||
  4978. containerRect.bottom < headerTop ||
  4979. containerRect.left > viewportWidth ||
  4980. containerRect.top > viewportHeight - headerBottom;
  4981.  
  4982. if (isOffscreen) {
  4983. // Move to default position if completely offscreen
  4984. this.move(10, headerTop + 10);
  4985. } else {
  4986. // Adjust position if partially offscreen
  4987. let newLeft = containerRect.left;
  4988. let newTop = containerRect.top;
  4989.  
  4990. if (containerRect.left < 0) {
  4991. newLeft = 0;
  4992. } else if (containerRect.right > viewportWidth) {
  4993. newLeft = viewportWidth - containerRect.width;
  4994. }
  4995.  
  4996. if (containerRect.top < headerTop) {
  4997. newTop = headerTop;
  4998. } else if (containerRect.bottom > viewportHeight - headerBottom) {
  4999. newTop = viewportHeight - headerBottom - containerRect.height;
  5000. }
  5001.  
  5002. if (newLeft !== containerRect.left || newTop !== containerRect.top) {
  5003. this.move(newLeft, newTop);
  5004. }
  5005. }
  5006. },
  5007.  
  5008. showAllControls: function() {
  5009. Player.$all(`.${ns}-controls [data-hide-id]`).forEach((e) => (e.style.display = null));
  5010. },
  5011.  
  5012. preventWrapping: function() {
  5013. // Reset display style first
  5014. Player.position.showAllControls();
  5015.  
  5016. if (!Player.config.preventControlWrapping) return;
  5017.  
  5018. const container = Player.$(`.${ns}-controls`);
  5019. const hideOrder = Player.position.setHideOrder();
  5020. let controls = Array.from(container.children).filter(el => el.hasAttribute('data-hide-id'));
  5021. let lastControl = controls[controls.length - 1];
  5022.  
  5023. // Get initial state
  5024. const containerWidth = container.clientWidth + 1; // +1 fix for Penumbra css theme
  5025. let contentWidth = Array.from(container.children).reduce((sum, el) => sum + el.clientWidth, 0);
  5026.  
  5027. const seekBar = document.querySelector(`.${ns}-seek-bar`);
  5028. const volumeBar = document.querySelector(`.${ns}-volume-bar`);
  5029. if(containerWidth <= 345) {
  5030. seekBar.style.margin = "0 0.4rem";
  5031. volumeBar.style.margin = "0 0.4rem";
  5032. } else {
  5033. seekBar.style.margin = "0 0.8rem";
  5034. volumeBar.style.margin = "0 0.8rem";
  5035. }
  5036.  
  5037. if (contentWidth <= containerWidth) return;
  5038.  
  5039. // Hide controls until content fits
  5040. let hideIndex = 0;
  5041. while (contentWidth > containerWidth && hideIndex < hideOrder.length) {
  5042. const controlToHide = hideOrder[hideIndex];
  5043. if (!controlToHide) continue;
  5044.  
  5045. controlToHide.style.display = "none";
  5046. controls = controls.filter(control => control !== controlToHide);
  5047.  
  5048. if (controlToHide === lastControl && controls.length > 0) {
  5049. lastControl = controls[controls.length - 1];
  5050. }
  5051.  
  5052. contentWidth = Array.from(container.children).reduce((sum, el) => sum + el.clientWidth, 0);
  5053. hideIndex++;
  5054. }
  5055. },
  5056.  
  5057. setHideOrder: function() {
  5058. // Reset to default if not set
  5059. if (!Array.isArray(Player.config.controlsHideOrder)) {
  5060. Player.settings.reset("controlsHideOrder");
  5061. }
  5062.  
  5063. const controlsContainer = Player.$(`.${ns}-controls`);
  5064.  
  5065. // Create priority map based on array position
  5066. const priorityMap = {};
  5067. Player.config.controlsHideOrder.forEach((control, index) => {
  5068. priorityMap[control] = index;
  5069. });
  5070.  
  5071. // Get all hideable controls, filter to only those in priorityMap, and sort by priority
  5072. Player.position.hideOrder = Array.from(controlsContainer.querySelectorAll('[data-hide-id]'))
  5073. .filter(element => element.getAttribute('data-hide-id') in priorityMap)
  5074. .sort((a, b) => {
  5075. const aPriority = priorityMap[a.getAttribute('data-hide-id')];
  5076. const bPriority = priorityMap[b.getAttribute('data-hide-id')];
  5077. return aPriority - bPriority;
  5078. });
  5079.  
  5080. return Player.position.hideOrder;
  5081. },
  5082.  
  5083. preventWrappingHeaderFooter: function() {
  5084. const container = Player.$(`.${ns}-footer`);
  5085. if (!container) return;
  5086.  
  5087. const containerWidth = container.clientWidth;
  5088. const footerUiBrackets = document.querySelectorAll(`.${ns}-footer .${ns}-ui-bracket`);
  5089. const footerText = document.querySelectorAll(`.${ns}-footer .${ns}-footer-text`);
  5090. const headerTitle = document.querySelectorAll(`.${ns}-header.${ns}-row .${ns}-col.${ns}-truncate-text span`);
  5091.  
  5092. // Hide or unhide
  5093. const footerBracketDisplay = containerWidth < 225 ? "none" : "";
  5094. const footerTextDisplay = containerWidth < 345 ? "none" : "";
  5095. const headerTitleDisplay = containerWidth < 268 ? "none" : "";
  5096.  
  5097. footerUiBrackets.forEach(el => el.style.display = footerBracketDisplay);
  5098. footerText.forEach(el => el.style.display = footerTextDisplay);
  5099. headerTitle.forEach(el => el.style.display = headerTitleDisplay);
  5100. },
  5101. };
  5102. }),
  5103. /* 15 - Thread Search
  5104. • Catalog scanning:
  5105. o Board selection
  5106. o Sound thread detection
  5107. • Displays:
  5108. o Table view (metadata)
  5109. o Board-style view (4chan X only)
  5110. */
  5111. (function(module, exports, __webpack_require__) {
  5112.  
  5113. const {
  5114. parseFileName
  5115. } = __webpack_require__(0);
  5116. const {
  5117. get
  5118. } = __webpack_require__(16);
  5119.  
  5120. const boardsURL = /*'https://a.4cdn.org/boards.json'*/'';
  5121. const catalogURL = /*'https://a.4cdn.org/%s/catalog.json'*/'';
  5122.  
  5123. module.exports = {
  5124. boardList: null,
  5125. soundThreads: null,
  5126. displayThreads: {},
  5127. selectedBoards: Board ? [Board] : ['a'],
  5128. showAllBoards: false,
  5129.  
  5130. delegatedEvents: {
  5131. click: {
  5132. [`.${ns}-fetch-threads-link`]: 'threads.fetch',
  5133. [`.${ns}-all-boards-link`]: 'threads.toggleBoardList'
  5134. },
  5135. keyup: {
  5136. [`.${ns}-threads-filter`]: e => Player.threads.filter(e.eventTarget.value)
  5137. },
  5138. change: {
  5139. [`.${ns}-threads input[type=checkbox]`]: 'threads.toggleBoard'
  5140. }
  5141. },
  5142.  
  5143. initialize: function() {
  5144. Player.threads.hasParser = is4chan && typeof Parser !== 'undefined';
  5145. // If the native Parser hasn't been intialised chuck customSpoiler on it so we can call it for threads.
  5146. // You shouldn't do things like this. We can fall back to the table view if it breaks though.
  5147. if (Player.threads.hasParser && !Parser.customSpoiler) {
  5148. Parser.customSpoiler = {};
  5149. }
  5150.  
  5151. Player.on('show', Player.threads._initialFetch);
  5152. Player.on('view', Player.threads._initialFetch);
  5153. Player.on('rendered', Player.threads.afterRender);
  5154. Player.on('config:threadsViewStyle', Player.threads.render);
  5155. },
  5156.  
  5157. /**
  5158. * Fetch the threads when the threads view is opened for the first time.
  5159. */
  5160. _initialFetch: function() {
  5161. if (Player.container && Player.config.viewStyle === 'threads' && Player.threads.boardList === null) {
  5162. Player.threads.fetchBoards(true);
  5163. }
  5164. },
  5165.  
  5166. render: function() {
  5167. if (Player.container) {
  5168. Player.$(`.${ns}-threads`).innerHTML = Player.templates.threads();
  5169. Player.threads.afterRender();
  5170. }
  5171. },
  5172.  
  5173. /**
  5174. * Render the threads and apply the board styling after the view is rendered.
  5175. */
  5176. afterRender: function() {
  5177. const threadList = Player.$(`.${ns}-thread-list`);
  5178. if (threadList) {
  5179. const bodyStyle = document.defaultView.getComputedStyle(document.body);
  5180. threadList.style.background = bodyStyle.backgroundColor;
  5181. threadList.style.backgroundImage = bodyStyle.backgroundImage;
  5182. threadList.style.backgroundRepeat = bodyStyle.backgroundRepeat;
  5183. threadList.style.backgroundPosition = bodyStyle.backgroundPosition;
  5184. }
  5185. Player.threads.renderThreads();
  5186. },
  5187.  
  5188. /**
  5189. * Render just the threads.
  5190. */
  5191. renderThreads: function() {
  5192. if (!Player.threads.hasParser || Player.config.threadsViewStyle === 'table') {
  5193. Player.$(`.${ns}-threads-body`).innerHTML = Player.templates.threadList();
  5194. } else {
  5195. try {
  5196. const list = Player.$(`.${ns}-thread-list`);
  5197. for (let board in Player.threads.displayThreads) {
  5198. // Create a board title
  5199. const boardConf = Player.threads.boardList.find(boardConf => boardConf.board === board);
  5200. const boardTitle = `/${boardConf.board}/ - ${boardConf.title}`;
  5201. createElement(`<div class="boardBanner"><div class="boardTitle">${boardTitle}</div></div>`, list);
  5202.  
  5203. // Add each thread for the board
  5204. const threads = Player.threads.displayThreads[board];
  5205. for (let i = 0; i < threads.length; i++) {
  5206. list.appendChild(Parser.buildHTMLFromJSON.call(Parser, threads[i], threads[i].board, true, true));
  5207.  
  5208. // Add a line under each thread
  5209. createElement('<hr style="clear: both">', list);
  5210. }
  5211. }
  5212. } catch (err) {
  5213. Player.logError('Unable to display the threads board view.', 'warning');
  5214. // If there was an error fall back to the table view.
  5215. Player.set('threadsViewStyle', 'table');
  5216. Player.renderThreads();
  5217. }
  5218. }
  5219. },
  5220.  
  5221. /**
  5222. * Render just the board selection.
  5223. */
  5224. renderBoards: function() {
  5225. Player.$(`.${ns}-thread-board-list`).innerHTML = Player.templates.threadBoards();
  5226. },
  5227.  
  5228. /**
  5229. * Toggle the threads view.
  5230. */
  5231. toggle: function(e) {
  5232. e && e.preventDefault();
  5233. if (Player.config.viewStyle === 'threads') {
  5234. Player.playlist.restore();
  5235. } else {
  5236. Player.display.setViewStyle('threads');
  5237. }
  5238. },
  5239.  
  5240. /**
  5241. * Switch between showing just the selected boards and all boards.
  5242. */
  5243. toggleBoardList: function() {
  5244. Player.threads.showAllBoards = !Player.threads.showAllBoards;
  5245. Player.$(`.${ns}-all-boards-link`).innerHTML = Player.threads.showAllBoards ? 'Selected Only' : 'Show All';
  5246. Player.threads.renderBoards();
  5247. },
  5248.  
  5249. /**
  5250. * Select/deselect a board.
  5251. */
  5252. toggleBoard: function(e) {
  5253. const board = e.eventTarget.value;
  5254. const selected = e.eventTarget.checked;
  5255. if (selected) {
  5256. !Player.threads.selectedBoards.includes(board) && Player.threads.selectedBoards.push(board);
  5257. } else {
  5258. Player.threads.selectedBoards = Player.threads.selectedBoards.filter(b => b !== board);
  5259. }
  5260. },
  5261.  
  5262. /**
  5263. * Fetch the board list from the 4chan API.
  5264. */
  5265. fetchBoards: async function(fetchThreads) {
  5266. Player.threads.loading = true;
  5267. Player.threads.render();
  5268. Player.threads.boardList = (await get(boardsURL)).boards;
  5269. if (fetchThreads) {
  5270. Player.threads.fetch();
  5271. } else {
  5272. Player.threads.loading = false;
  5273. Player.threads.render();
  5274. }
  5275. },
  5276.  
  5277. /**
  5278. * Fetch the catalog for each selected board and search for sounds in OPs.
  5279. */
  5280. fetch: async function(e) {
  5281. e && e.preventDefault();
  5282. Player.threads.loading = true;
  5283. Player.threads.render();
  5284. if (!Player.threads.boardList) {
  5285. try {
  5286. await Player.threads.fetchBoards();
  5287. } catch (err) {
  5288. Player.logError('Failed to fetch the boards configuration.');
  5289. console.error(err);
  5290. return;
  5291. }
  5292. }
  5293. const allThreads = [];
  5294. try {
  5295. await Promise.all(Player.threads.selectedBoards.map(async board => {
  5296. const boardConf = Player.threads.boardList.find(boardConf => boardConf.board === board);
  5297. if (!boardConf) {
  5298. return;
  5299. }
  5300. const pages = boardConf && await get(catalogURL.replace('%s', board));
  5301. (pages || []).forEach(({
  5302. page,
  5303. threads
  5304. }) => {
  5305. allThreads.push(...threads.map(thread => Object.assign(thread, {
  5306. board,
  5307. page,
  5308. ws_board: boardConf.ws_board
  5309. })));
  5310. });
  5311. }));
  5312.  
  5313. Player.threads.soundThreads = allThreads.filter(thread => {
  5314. const sounds = parseFileName(thread.filename, `https://i.4cdn.org/${thread.board}/${thread.tim}${thread.ext}`, thread.no, `https://i.4cdn.org/${thread.board}/${thread.tim}s${thread.ext}`, thread.md5);
  5315. return sounds.length;
  5316. });
  5317. } catch (err) {
  5318. Player.logError('Failed to search for sounds threads.');
  5319. console.error(err);
  5320. }
  5321. Player.threads.loading = false;
  5322. Player.threads.filter(Player.$(`.${ns}-threads-filter`).value, true);
  5323. Player.threads.render();
  5324. },
  5325.  
  5326. /**
  5327. * Apply the filter input to the already fetched threads.
  5328. */
  5329. filter: function(search, skipRender) {
  5330. Player.threads.filterValue = search || '';
  5331. if (Player.threads.soundThreads === null) {
  5332. return;
  5333. }
  5334. Player.threads.displayThreads = Player.threads.soundThreads.reduce((threadsByBoard, thread) => {
  5335. if (!search || thread.sub && thread.sub.includes(search) || thread.com && thread.com.includes(search)) {
  5336. threadsByBoard[thread.board] || (threadsByBoard[thread.board] = []);
  5337. threadsByBoard[thread.board].push(thread);
  5338. }
  5339. return threadsByBoard;
  5340. }, {});
  5341. !skipRender && Player.threads.renderThreads();
  5342. }
  5343. };
  5344.  
  5345.  
  5346. }),
  5347. /* 16 - Network Utilities
  5348. • Cached requests:
  5349. o get(): GM_xmlHttpRequest wrapper
  5350. o Conditional requests
  5351. o JSON handling
  5352. */
  5353. (function(module, exports) {
  5354.  
  5355. const cache = {};
  5356.  
  5357. module.exports = {
  5358. get
  5359. };
  5360.  
  5361. async function get(url) {
  5362. return new Promise(function(resolve, reject) {
  5363. const headers = {};
  5364. if (cache[url]) {
  5365. headers['If-Modified-Since'] = cache[url].lastModified;
  5366. }
  5367. GM.xmlHttpRequest({
  5368. method: 'GET',
  5369. url,
  5370. headers,
  5371. responseType: 'json',
  5372. onload: response => {
  5373. if (response.status >= 200 && response.status < 300) {
  5374. cache[url] = {
  5375. lastModified: response.responseHeaders['last-modified'],
  5376. response: response.response
  5377. };
  5378. }
  5379. resolve(response.status === 304 ? cache[url].response : response.response);
  5380. },
  5381. onerror: reject
  5382. });
  5383. });
  5384. }
  5385.  
  5386.  
  5387. }),
  5388. /* 17 - Template System
  5389. • Dynamic UI generation:
  5390. o Button definitions
  5391. o Template parsing
  5392. o Conditional rendering
  5393. • Handles all user-customizable layouts
  5394. */
  5395. (function(module, exports, __webpack_require__) {
  5396.  
  5397. const buttons = __webpack_require__(18);
  5398.  
  5399. // Regex for replacements
  5400. const playingRE = /p: ?{([^}]*)}/g;
  5401. const hoverRE = /h: ?{([^}]*)}/g;
  5402. const buttonRE = new RegExp(`(${buttons.map(option => option.tplName).join('|')})-(?:button|link|icon)(?:\\:"([^"]+?)")?`, 'g');
  5403. const soundNameRE = /sound-name/g;
  5404. const soundIndexRE = /sound-index/g;
  5405. const soundCountRE = /sound-count/g;
  5406.  
  5407. // Hold information on which config values components templates depend on.
  5408. const componentDeps = [];
  5409.  
  5410. module.exports = {
  5411. buttons,
  5412.  
  5413. delegatedEvents: {
  5414. click: {
  5415. [`.${ns}-playing-jump-link`]: () => Player.playlist.scrollToPlaying('center'),
  5416. [`.${ns}-viewStyle-button`]: 'playlist.toggleView',
  5417. [`.${ns}-hoverImages-button`]: 'playlist.toggleHoverImages',
  5418. [`.${ns}-remove-link`]: 'userTemplate._handleRemove',
  5419. [`.${ns}-filter-link`]: 'userTemplate._handleFilter',
  5420. [`.${ns}-download-link`]: 'userTemplate._handleDownload',
  5421. [`.${ns}-shuffle-button`]: 'userTemplate._handleShuffle',
  5422. [`.${ns}-repeat-button`]: 'userTemplate._handleRepeat',
  5423. [`.${ns}-reload-button`]: noDefault('playlist.refresh'),
  5424. [`.${ns}-add-button`]: noDefault(() => Player.$(`.${ns}-file-input`).click()),
  5425. [`.${ns}-item-menu-button`]: 'userTemplate._handleMenu',
  5426. [`.${ns}-threads-button`]: 'threads.toggle',
  5427. [`.${ns}-config-button`]: 'settings.toggle'
  5428. },
  5429. change: {
  5430. [`.${ns}-file-input`]: 'userTemplate._handleFileSelect'
  5431. }
  5432. },
  5433.  
  5434. undelegatedEvents: {
  5435. click: {
  5436. body: 'userTemplate._closeMenus'
  5437. },
  5438. keydown: {
  5439. body: e => e.key === 'Escape' && Player.userTemplate._closeMenus()
  5440. }
  5441. },
  5442.  
  5443. initialize: function() {
  5444. Player.on('config', Player.userTemplate._handleConfig);
  5445. Player.on('playsound', () => Player.userTemplate._handleEvent('playsound'));
  5446. Player.on('add', () => Player.userTemplate._handleEvent('add'));
  5447. Player.on('remove', () => Player.userTemplate._handleEvent('remove'));
  5448. Player.on('order', () => Player.userTemplate._handleEvent('order'));
  5449. Player.on('show', () => Player.userTemplate._handleEvent('show'));
  5450. Player.on('hide', () => Player.userTemplate._handleEvent('hide'));
  5451. },
  5452.  
  5453. /**
  5454. * Build a user template.
  5455. */
  5456. build: function(data) {
  5457. const outerClass = data.outerClass || '';
  5458. let name = (data.template === Player.config.headerTemplate)
  5459. ? (data.sound && data.sound.post || data.defaultName)
  5460. : (data.sound && data.sound.title || data.defaultName);
  5461. if(data.template === Player.config.headerTemplate) {
  5462. if(window.mediaStatus === "Loading") name = 'Loading...';
  5463. if(window.mediaStatus === "Error") name = 'error';
  5464. }
  5465. const postID = data.sound && data.sound.post || data.defaultName;
  5466.  
  5467. // Apply common template replacements
  5468. let html = data.template
  5469. .replace(playingRE, Player.playing && Player.playing === data.sound ? '$1' : '')
  5470. .replace(hoverRE, `<span class="${ns}-hover-display ${outerClass}">$1</span>`)
  5471. .replace(buttonRE, function(full, type, text) {
  5472. let buttonConf = buttons.find(conf => conf.tplName === type);
  5473. if (buttonConf.requireSound && !data.sound || buttonConf.showIf && !buttonConf.showIf(data)) {
  5474. return '';
  5475. }
  5476. // If the button config has sub values then extend the base config with the selected sub value.
  5477. // Which value is to use is taken from the `property` in the base config of the player config.
  5478. // This gives us different state displays.
  5479. if (buttonConf.values) {
  5480. buttonConf = {
  5481. ...buttonConf,
  5482. ...buttonConf.values[_get(Player.config, buttonConf.property)] || buttonConf.values[Object.keys(buttonConf.values)[0]]
  5483. };
  5484. }
  5485. const attrs = typeof buttonConf.attrs === 'function' ? buttonConf.attrs(data) : buttonConf.attrs || [];
  5486. attrs.some(attr => attr.startsWith('href')) || attrs.push('href=javascript:;');
  5487. (buttonConf.class || outerClass) && attrs.push(`class="${buttonConf.class || ''} ${outerClass || ''}"`);
  5488.  
  5489. if (!text) {
  5490. text = buttonConf.icon ?
  5491. `<svg xmlns="http://www.w3.org/2000/svg" ${buttonConf.icon}></svg>`+buttonConf.text :
  5492. buttonConf.text;
  5493. }
  5494.  
  5495. if (/-icon$/.test(full)) return `<div ${attrs.join(' ')}>${text}</div>`;
  5496. return `<a ${attrs.join(' ')} draggable="false">${text}</a>`;
  5497. })
  5498. .replace(soundNameRE, name ? `<div class="fc-sounds-col fc-sounds-truncate-text"><span title="${postID}">${name}</span></div>` : '')
  5499. .replace(soundIndexRE, data.sound ? Player.sounds.indexOf(data.sound) + 1 : 0)
  5500. .replace(soundCountRE, Player.sounds.length)
  5501. .replace(/%v/g, "2.3.0");
  5502.  
  5503. // Apply any specific replacements
  5504. if (data.replacements) {
  5505. for (let k of Object.keys(data.replacements)) {
  5506. html = html.replace(new RegExp(k, 'g'), data.replacements[k]);
  5507. }
  5508. }
  5509.  
  5510. return html;
  5511. },
  5512.  
  5513. /**
  5514. * Sets up a components to render when the template or values within it are changed.
  5515. */
  5516. maintain: function(component, property, alwaysRenderConfigs = [], alwaysRenderEvents = []) {
  5517. componentDeps.push({
  5518. component,
  5519. property,
  5520. ...Player.userTemplate.findDependencies(property, null),
  5521. alwaysRenderConfigs,
  5522. alwaysRenderEvents
  5523. });
  5524. },
  5525.  
  5526. /**
  5527. * Find all the config dependent values in a template.
  5528. */
  5529. findDependencies: function(property, template) {
  5530. template || (template = _get(Player.config, property));
  5531. // Figure out what events should trigger a render.
  5532. const events = [];
  5533.  
  5534. // add/remove should render templates showing the count.
  5535. // playsound should render templates showing the playing sounds name/index or dependent on something playing.
  5536. // order should render templates showing a sounds index.
  5537. const hasCount = soundCountRE.test(template);
  5538. const hasName = soundNameRE.test(template);
  5539. const hasIndex = soundIndexRE.test(template);
  5540. const hasPlaying = playingRE.test(template);
  5541. hasCount && events.push('add', 'remove');
  5542. (hasPlaying || property !== 'rowTemplate' && (hasName || hasIndex)) && events.push('playsound');
  5543. hasIndex && events.push('order');
  5544.  
  5545. // Find which buttons the template includes that are dependent on config values.
  5546. const config = [];
  5547. let match;
  5548. while ((match = buttonRE.exec(template)) !== null) {
  5549. // If user text is given then the display doesn't change.
  5550. if (!match[2]) {
  5551. let type = match[1];
  5552. let buttonConf = buttons.find(conf => conf.tplName === type);
  5553. if (buttonConf.property) {
  5554. config.push(buttonConf.property);
  5555. }
  5556. }
  5557. }
  5558.  
  5559. return {
  5560. events,
  5561. config
  5562. };
  5563. },
  5564.  
  5565. /**
  5566. * When a config value is changed check if any component dependencies are affected.
  5567. */
  5568. _handleConfig: function(property, value) {
  5569. // Check if a template for a components was updated.
  5570. componentDeps.forEach(depInfo => {
  5571. if (depInfo.property === property) {
  5572. Object.assign(depInfo, Player.userTemplate.findDependencies(property, value));
  5573. depInfo.component.render();
  5574. }
  5575. });
  5576. // Check if any components are dependent on the updated property.
  5577. componentDeps.forEach(depInfo => {
  5578. if (depInfo.alwaysRenderConfigs.includes(property) || depInfo.config.includes(property)) {
  5579. depInfo.component.render();
  5580. }
  5581. });
  5582. },
  5583.  
  5584. /**
  5585. * When a player event is triggered check if any component dependencies are affected.
  5586. */
  5587. _handleEvent: function(type) {
  5588. // Check if any components are dependent on the updated property.
  5589. componentDeps.forEach(depInfo => {
  5590. if (depInfo.alwaysRenderEvents.includes(type) || depInfo.events.includes(type)) {
  5591. depInfo.component.render();
  5592. }
  5593. });
  5594. },
  5595.  
  5596. /**
  5597. * Add local files.
  5598. */
  5599. _handleFileSelect: function(e) {
  5600. e.preventDefault();
  5601. const input = e.eventTarget;
  5602. Player.playlist.addFromFiles(input.files);
  5603. },
  5604.  
  5605. /**
  5606. * Toggle the repeat style.
  5607. */
  5608. _handleRepeat: function(e) {
  5609. try {
  5610. e.preventDefault();
  5611. const values = ['all', 'one', 'none'];
  5612. const current = values.indexOf(Player.config.repeat);
  5613. Player.set('repeat', values[(current + 4) % 3]);
  5614. } catch (err) {
  5615. Player.logError('There was an error changing the repeat setting. Please check the console for details.', 'warning');
  5616. console.error('[8chan sounds player]', err);
  5617. }
  5618. },
  5619.  
  5620. /**
  5621. * Toggle the shuffle style.
  5622. */
  5623. _handleShuffle: function(e) {
  5624. try {
  5625. e.preventDefault();
  5626. Player.set('shuffle', !Player.config.shuffle);
  5627. Player.header.render();
  5628.  
  5629. // Update the play order.
  5630. if (!Player.config.shuffle) {
  5631. Player.sounds.sort((a, b) => Player.compareIds(a.id, b.id));
  5632. } else {
  5633. const sounds = Player.sounds;
  5634. for (let i = sounds.length - 1; i > 0; i--) {
  5635. const j = Math.floor(Math.random() * (i + 1));
  5636. [sounds[i], sounds[j]] = [sounds[j], sounds[i]];
  5637. }
  5638. }
  5639. Player.trigger('order');
  5640. } catch (err) {
  5641. Player.logError('There was an error changing the shuffle setting. Please check the console for details.', 'warning');
  5642. console.error('[8chan sounds player]', err);
  5643. }
  5644. },
  5645.  
  5646. /**
  5647. * Display an item menu.
  5648. */
  5649. _handleMenu: function(e) {
  5650. e.preventDefault();
  5651. e.stopPropagation();
  5652. const x = e.clientX;
  5653. const y = e.clientY;
  5654. const id = e.eventTarget.getAttribute('data-id');
  5655. const sound = Player.sounds.find(s => s.id === id);
  5656.  
  5657. // Add row item menus to the list container. Append to the container otherwise.
  5658. const listContainer = e.eventTarget.closest(`.${ns}-list-container`);
  5659. const parent = listContainer || Player.container;
  5660.  
  5661. // Create the menu.
  5662. const dialog = createElement(Player.templates.itemMenu({
  5663. x,
  5664. y,
  5665. sound
  5666. }), parent);
  5667.  
  5668. parent.appendChild(dialog);
  5669.  
  5670. // Make sure it's within the page.
  5671. const style = document.defaultView.getComputedStyle(dialog);
  5672. const width = parseInt(style.width, 10);
  5673. const height = parseInt(style.height, 10);
  5674. // Show the dialog to the left of the cursor, if there's room.
  5675. if (x - width > 0) {
  5676. dialog.style.left = x - width + 'px';
  5677. }
  5678. // Move the dialog above the cursor if it's off screen.
  5679. if (y + height > document.documentElement.clientHeight - 40) {
  5680. dialog.style.top = y - height + 'px';
  5681. }
  5682. // Add the focused class handler
  5683. dialog.querySelectorAll('.entry').forEach(el => {
  5684. el.addEventListener('mouseenter', Player.userTemplate._setFocusedMenuItem);
  5685. el.addEventListener('mouseleave', Player.userTemplate._unsetFocusedMenuItem);
  5686. });
  5687.  
  5688. Player.trigger('menu-open', dialog);
  5689. },
  5690.  
  5691. /**
  5692. * Close any open menus, except for one belonging to an item that was clicked.
  5693. */
  5694. _closeMenus: function() {
  5695. document.querySelectorAll(`.${ns}-item-menu`).forEach(menu => {
  5696. menu.parentNode.removeChild(menu);
  5697. Player.trigger('menu-close', menu);
  5698. });
  5699. },
  5700.  
  5701. _setFocusedMenuItem: function(e) {
  5702. e.currentTarget.classList.add('focused');
  5703. const submenu = e.currentTarget.querySelector('.submenu');
  5704. // Move the menu to the other side if there isn't room.
  5705. if (submenu && submenu.getBoundingClientRect().right > document.documentElement.clientWidth) {
  5706. submenu.style.inset = '0px auto auto -100%';
  5707. }
  5708. },
  5709.  
  5710. _unsetFocusedMenuItem: function(e) {
  5711. e.currentTarget.classList.remove('focused');
  5712. },
  5713.  
  5714. _handleFilter: function(e) {
  5715. e.preventDefault();
  5716. let filter = e.eventTarget.getAttribute('data-filter');
  5717. if (filter) {
  5718. Player.set('filters', Player.config.filters.concat(filter));
  5719. }
  5720. },
  5721.  
  5722. _handleDownload: function(e) {
  5723. const src = e.eventTarget.getAttribute('data-src');
  5724. const name = e.eventTarget.getAttribute('data-name') || new URL(src).pathname.split('/').pop();
  5725.  
  5726. GM.xmlHttpRequest({
  5727. method: 'GET',
  5728. url: src,
  5729. responseType: 'blob',
  5730. onload: response => {
  5731. const a = createElement(`<a href="${URL.createObjectURL(response.response)}" download="${name}" rel="noopener" target="_blank"></a>`);
  5732. a.click();
  5733. URL.revokeObjectURL(a.href);
  5734. },
  5735. onerror: () => Player.logError('There was an error downloading.', 'warning')
  5736. });
  5737. },
  5738.  
  5739. _handleRemove: function(e) {
  5740. const id = e.eventTarget.getAttribute('data-id');
  5741. const sound = id && Player.sounds.find(sound => sound.id === '' + id);
  5742. sound && Player.remove(sound);
  5743. },
  5744. };
  5745.  
  5746.  
  5747. }),
  5748. /* 18 - Button Definitions
  5749. • All control buttons:
  5750. o Icons
  5751. o Behavior flags
  5752. o State variants
  5753. • Organized by function (playback, navigation, etc.)
  5754. */
  5755. (function(module, exports) {
  5756.  
  5757. module.exports = [{
  5758. property: 'repeat',
  5759. tplName: 'repeat',
  5760. class: `${ns}-ui-button ${ns}-repeat-button`,
  5761. values: {
  5762. all: {
  5763. attrs: ['title="Repeat All"'],
  5764. text: '',
  5765. icon: 'width="17.6px" height="16px" viewBox="1 0 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-repeat"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 12v-3a3 3 0 0 1 3 -3h13m-3 -3l3 3l-3 3" /><path d="M20 12v3a3 3 0 0 1 -3 3h-13m3 3l-3 -3l3 -3" />'
  5766. },
  5767. one: {
  5768. attrs: ['title="Repeat One"'],
  5769. text: '',
  5770. icon: 'width="17.6px" height="16px" viewBox="1 0 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-repeat-once"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 12v-3a3 3 0 0 1 3 -3h13m-3 -3l3 3l-3 3" /><path d="M20 12v3a3 3 0 0 1 -3 3h-13m3 3l-3 -3l3 -3" /><path d="M11 11l1 -1v4" />'
  5771. },
  5772. none: {
  5773. attrs: ['title="No Repeat"'],
  5774. text: '',
  5775. icon: 'width="17.6px" height="16px" viewBox="1 0 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-repeat-off"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 12v-3c0 -1.336 .873 -2.468 2.08 -2.856m3.92 -.144h10m-3 -3l3 3l-3 3" /><path d="M20 12v3a3 3 0 0 1 -.133 .886m-1.99 1.984a3 3 0 0 1 -.877 .13h-13m3 3l-3 -3l3 -3" /><path d="M3 3l18 18" />'
  5776. }
  5777. }
  5778. },
  5779. {
  5780. property: 'shuffle',
  5781. tplName: 'shuffle',
  5782. class: `${ns}-ui-button ${ns}-shuffle-button`,
  5783. values: {
  5784. true: {
  5785. attrs: ['title="Shuffled"'],
  5786. text: '',
  5787. icon: 'width="17.6px" height="16px" viewBox="1 0 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrows-shuffle-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 4l3 3l-3 3" /><path d="M18 20l3 -3l-3 -3" /><path d="M3 7h3a5 5 0 0 1 5 5a5 5 0 0 0 5 5h5" /><path d="M3 17h3a5 5 0 0 0 5 -5a5 5 0 0 1 5 -5h5" />',
  5788. },
  5789. false: {
  5790. attrs: ['title="Ordered"'],
  5791. text: '',
  5792. icon: 'width="17.6px" height="16px" viewBox="1 0 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrows-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M21 17l-18 0" /><path d="M18 4l3 3l-3 3" /><path d="M18 20l3 -3l-3 -3" /><path d="M21 7l-18 0" />',
  5793. }
  5794. }
  5795. },
  5796. {
  5797. property: 'viewStyle',
  5798. tplName: 'playlist',
  5799. class: `${ns}-ui-button ${ns}-viewStyle-button`,
  5800. values: {
  5801. playlist: {
  5802. attrs: ['title="Show Playlist Enabled"'],
  5803. text: '',
  5804. icon: 'width="17.6px" height="16px" viewBox="1 0 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-playlist"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M17 17v-13h4" /><path d="M13 5h-10" /><path d="M3 9l10 0" /><path d="M9 13h-6" />',
  5805. },
  5806. gallery: {
  5807. attrs: ['title="Show Gallery Enabled"'],
  5808. text: '',
  5809. icon: 'width="17.6px" height="16px" viewBox="1 0 22 23" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-library-photo"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 3m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" /><path d="M4.012 7.26a2.005 2.005 0 0 0 -1.012 1.737v10c0 1.1 .9 2 2 2h10c.75 0 1.158 -.385 1.5 -1" /><path d="M17 7h.01" /><path d="M7 13l3.644 -3.644a1.21 1.21 0 0 1 1.712 0l3.644 3.644" /><path d="M15 12l1.644 -1.644a1.21 1.21 0 0 1 1.712 0l2.644 2.644" />',
  5810. },
  5811. image: {
  5812. attrs: ['title="Show Playlist Disabled"'],
  5813. text: '',
  5814. icon: 'width="17.6px" height="16px" viewBox="1 0 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-playlist-off"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 14a3 3 0 1 0 3 3" /><path d="M17 13v-9h4" /><path d="M13 5h-4m-4 0h-2" /><path d="M3 9h6" /><path d="M9 13h-6" /><path d="M3 3l18 18" />',
  5815. }
  5816. }
  5817. },
  5818. {
  5819. property: 'hoverImages',
  5820. tplName: 'hover-images',
  5821. class: `${ns}-ui-button ${ns}-hoverImages-button`,
  5822. values: {
  5823. true: {
  5824. attrs: ['title="Hover Images Enabled"'],
  5825. text: '',
  5826. icon: 'width="17.6px" height="16px" viewBox="1 0 22 23" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-photo-check"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 8h.01" /><path d="M11.5 21h-5.5a3 3 0 0 1 -3 -3v-12a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v7" /><path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l4 4" /><path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l.5 .5" /><path d="M15 19l2 2l4 -4" />',
  5827. },
  5828. false: {
  5829. attrs: ['title="Hover Images Disabled"'],
  5830. text: '',
  5831. icon: 'width="17.6px" height="16px" viewBox="1 0 22 23" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-photo-x"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 8h.01" /><path d="M13 21h-7a3 3 0 0 1 -3 -3v-12a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v7" /><path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l3 3" /><path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0" /><path d="M22 22l-5 -5" /><path d="M17 22l5 -5" />',
  5832. }
  5833. }
  5834. },
  5835. {
  5836. tplName: 'add',
  5837. class: `${ns}-ui-button ${ns}-add-button`,
  5838. icon: 'width="17.6px" height="16px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-plus"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" />',
  5839. text: '',
  5840. attrs: ['title="Add local files"'],
  5841. },
  5842. {
  5843. tplName: 'reload',
  5844. class: `${ns}-ui-button ${ns}-reload-button`,
  5845. icon: 'width="17.6px" height="16px" viewBox="2 1 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-reload"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747" /><path d="M20 4v5h-5" />',
  5846. text: '',
  5847. attrs: ['title="Reload the playlist"'],
  5848. },
  5849. {
  5850. tplName: 'settings',
  5851. class: `${ns}-ui-button ${ns}-config-button`,
  5852. icon: 'width="17.6px" height="16px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-settings"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" /><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />',
  5853. text: '',
  5854. attrs: ['title="Settings"'],
  5855. },
  5856. {
  5857. tplName: 'threads',
  5858. class: `${ns}-ui-button ${ns}-threads-button`,
  5859. icon: 'width="17.6px" height="16px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-search"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" />',
  5860. text: '',
  5861. attrs: ['title="Threads"'],
  5862. },
  5863. {
  5864. tplName: 'close',
  5865. class: `${ns}-ui-button ${ns}-close-button`,
  5866. icon: 'width="17.6px" height="16px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-x"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 5a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14z" /><path d="M9 9l6 6m0 -6l-6 6" />',
  5867. text: '',
  5868. attrs: ['title="Hide the player"'],
  5869. },
  5870. {
  5871. tplName: 'playing',
  5872. requireSound: true,
  5873. class: `${ns}-ui-button ${ns}-playing-jump-link`,
  5874. text: 'Playing',
  5875. attrs: ['title="Scroll the playlist currently playing sound."'],
  5876. },
  5877. {
  5878. tplName: 'post',
  5879. class: `${ns}-ui-button ${ns}-post-button`,
  5880. requireSound: true,
  5881. icon: 'width="16px" height="14px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-message"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 9h8" /><path d="M8 13h6" /><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z" />',
  5882. text: '',
  5883. showIf: data => data.sound.post,
  5884. attrs: data => [
  5885. `href=${'#' + (is4chan ? 'p' : '') + data.sound.post}`,
  5886. 'title="Jump to the post for the current sound"',
  5887. ],
  5888. },
  5889. {
  5890. tplName: 'image',
  5891. class: `${ns}-ui-button ${ns}-image-button`,
  5892. requireSound: true,
  5893. icon: 'width="16px" height="14px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-photo"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 8h.01" /><path d="M12 21h-6a3 3 0 0 1 -3 -3v-12a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v7" /><path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l3 3" /><path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0" /><path d="M16 22l5 -5" /><path d="M21 21.5v-4.5h-4.5" />',
  5894. text: '',
  5895. attrs: data => [
  5896. `href=${data.sound.image}`,
  5897. 'title="Open the image in a new tab"',
  5898. 'target="_blank"',
  5899. ],
  5900. },
  5901. {
  5902. tplName: 'sound',
  5903. class: `${ns}-ui-button ${ns}-sound-button`,
  5904. requireSound: true,
  5905. href: data => data.sound.src,
  5906. icon: 'width="16px" height="14px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-music"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 17a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /><path d="M9 17v-13h10v9" /><path d="M9 8h10" /><path d="M16 22l5 -5" /><path d="M21 21.5v-4.5h-4.5" />',
  5907. text: '',
  5908. attrs: data => [
  5909. `href=${data.sound.src}`,
  5910. 'title="Open the sound in a new tab"',
  5911. 'target="blank"',
  5912. ],
  5913. },
  5914. {
  5915. tplName: 'dl-image',
  5916. requireSound: true,
  5917. class: `${ns}-ui-button ${ns}-download-link`,
  5918. icon: 'width="16px" height="14px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-photo-down"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 8h.01" /><path d="M12.5 21h-6.5a3 3 0 0 1 -3 -3v-12a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v6.5" /><path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l4 4" /><path d="M14 14l1 -1c.653 -.629 1.413 -.815 2.13 -.559" /><path d="M19 16v6" /><path d="M22 19l-3 3l-3 -3" />',
  5919. text: '',
  5920. attrs: data => [
  5921. 'title="Download the image with the original filename"',
  5922. `data-src="${data.sound.image}"`,
  5923. `data-name="${data.sound.filename}"`,
  5924. ],
  5925. },
  5926. {
  5927. tplName: 'dl-sound',
  5928. requireSound: true,
  5929. class: `${ns}-ui-button ${ns}-download-link`,
  5930. icon: 'width="16px" height="14px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-music-down"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 17a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /><path d="M9 17v-13h10v8" /><path d="M9 8h10" /><path d="M19 16v6" /><path d="M22 19l-3 3l-3 -3" />',
  5931. text: '',
  5932. attrs: data => [
  5933. 'title="Download the sound"',
  5934. `data-src="${data.sound.src}"`,
  5935. ],
  5936. },
  5937. {
  5938. tplName: 'filter-image',
  5939. requireSound: true,
  5940. class: `${ns}-ui-button ${ns}-ui-button ${ns}-filter-link`,
  5941. icon: 'width="16px" height="14px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-filter"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4h16v2.172a2 2 0 0 1 -.586 1.414l-4.414 4.414v7l-6 2v-8.5l-4.48 -4.928a2 2 0 0 1 -.52 -1.345v-2.227z" />',
  5942. text: '',
  5943. showIf: data => data.sound.imageMD5,
  5944. attrs: data => [
  5945. 'title="Add the image MD5 to the filters."',
  5946. `data-filter="${data.sound.imageMD5}"`,
  5947. ],
  5948. },
  5949. {
  5950. tplName: 'filter-sound',
  5951. requireSound: true,
  5952. class: `${ns}-ui-button ${ns}-filter-link`,
  5953. icon: 'width="16px" height="14px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-filter"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4h16v2.172a2 2 0 0 1 -.586 1.414l-4.414 4.414v7l-6 2v-8.5l-4.48 -4.928a2 2 0 0 1 -.52 -1.345v-2.227z" />',
  5954. text: '',
  5955. attrs: data => [
  5956. 'title="Add the sound URL to the filters."',
  5957. `data-filter="${data.sound.src.replace(/^(https?:)?\/\//, '')}"`,
  5958. ],
  5959. },
  5960. {
  5961. tplName: 'remove',
  5962. requireSound: true,
  5963. class: `${ns}-ui-button ${ns}-remove-link`,
  5964. icon: 'width="16px" height="14px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-trash"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 7l16 0" /><path d="M10 11l0 6" /><path d="M14 11l0 6" /><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" /><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />',
  5965. text: '',
  5966. attrs: data => [
  5967. 'title="Filter the image."',
  5968. `data-id="${data.sound.id}"`,
  5969. ],
  5970. },
  5971. {
  5972. tplName: 'menu',
  5973. requireSound: true,
  5974. class: `${ns}-ui-button ${ns}-item-menu-button`,
  5975. icon: '',
  5976. text: '▼',
  5977. attrs: data => [`data-id=${data.sound.id}`],
  5978. },
  5979. {
  5980. tplName: 'ui-bracketL',
  5981. class: `${ns}-ui-icon ${ns}-ui-bracket ${ns}-ui-bracketL-icon`,
  5982. icon: 'width="12.6px" height="14px" viewBox="0 4 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-compact-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M13 20l-3 -8l3 -8" />',
  5983. text: '',
  5984. },
  5985. {
  5986. tplName: 'ui-bracketR',
  5987. class: `${ns}-ui-icon ${ns}-ui-bracket ${ns}-ui-bracketR-icon`,
  5988. icon: 'width="12.6px" height="14px" viewBox="8 4 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-compact-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11 4l3 8l-3 8" />',
  5989. text: '',
  5990. },
  5991. {
  5992. tplName: 'ui-files',
  5993. class: `${ns}-ui-icon ${ns}-ui-files-icon`,
  5994. icon: 'width="12px" height="14px" viewBox="1 0 22 22" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-files"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 3v4a1 1 0 0 0 1 1h4" /><path d="M18 17h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h4l5 5v7a2 2 0 0 1 -2 2z" /><path d="M16 17v2a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h2" />',
  5995. text: '',
  5996. attrs: data => [
  5997. 'title="Files"',
  5998. ],
  5999. },
  6000. {
  6001. tplName: 'sound-tag-toggle',
  6002. class: `${ns}-ui-button ${ns}-sound-tag-toggle-button`,
  6003. property: 'showSoundTagOnly',
  6004. values: {
  6005. true: {
  6006. attrs: ['title="Show all posts"'],
  6007. text: '',
  6008. icon: 'width="16px" height="14px" viewBox="1 1 22 22" fill="currentColor" stroke="currentColor" stroke-width="-0.1" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-filled icon-tabler-filter"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M20 3h-16a1 1 0 0 0 -1 1v2.227l.008 .223a3 3 0 0 0 .772 1.795l4.22 4.641v8.114a1 1 0 0 0 1.316 .949l6 -2l.108 -.043a1 1 0 0 0 .576 -.906v-6.586l4.121 -4.12a3 3 0 0 0 .879 -2.123v-2.171a1 1 0 0 0 -1 -1z" />',
  6009. },
  6010. false: {
  6011. attrs: ['title="Show only sound posts"'],
  6012. text: '',
  6013. icon: 'width="16px" height="14px" viewBox="1 1 22 22" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-filter"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4h16v2.172a2 2 0 0 1 -.586 1.414l-4.414 4.414v7l-6 2v-8.5l-4.48 -4.928a2 2 0 0 1 -.52 -1.345v-2.227z" />',
  6014. }
  6015. }
  6016. },
  6017. ];
  6018. }),
  6019. /* 19 - Templates
  6020. Main player structure */
  6021. (function(module, exports) {
  6022.  
  6023. module.exports = (data = {}) => `<div id="${ns}-container" data-view-style="${Player.config.viewStyle}" style="top: 30px; left: 0px; width: 350px; display: none;">
  6024. <div class="${ns}-header ${ns}-row">
  6025. ${Player.templates.header(data)}
  6026. </div>
  6027. <div class="${ns}-view-container">
  6028. <div class="${ns}-player ${!Player.config.hoverImages ? `${ns}-hide-hover-image` : ''}" ">
  6029. ${Player.templates.player(data)}
  6030. </div>
  6031. <div class="${ns}-settings ${ns}-panel" style="height: 400px">
  6032. ${Player.templates.settings(data)}
  6033. </div>
  6034. <div class="${ns}-threads ${ns}-panel" style="height: 400px">
  6035. ${Player.templates.threads(data)}
  6036. </div>
  6037. </div>
  6038. <div class="${ns}-footer">
  6039. ${Player.templates.footer(data)}
  6040. </div>
  6041. <input class="${ns}-file-input" type="file" style="display: none" accept="image/*,.webm,.mp4" multiple>
  6042. </div>`
  6043.  
  6044. }),
  6045. /* 20 - Templates
  6046. Control bars */
  6047. (function(module, exports) {
  6048.  
  6049. module.exports = (data = {}) => `<div class="${ns}-col-auto" style="padding: 0 0 0 0.25rem;">
  6050. <div class="${ns}-media-control ${ns}-previous-button" data-hide-id="previous">
  6051. <div class="${ns}-previous-button-display"></div>
  6052. </div>
  6053. <div class="${ns}-media-control ${ns}-play-button" data-hide-id="play">
  6054. <div class="${ns}-play-button-display ${!Player.audio || Player.audio.paused ? `${ns}-play` : ''}"></div>
  6055. </div>
  6056. <div class="${ns}-media-control ${ns}-next-button" data-hide-id="next">
  6057. <div class="${ns}-next-button-display"></div>
  6058. </div>
  6059. </div>
  6060. <div class="${ns}-col" data-hide-id="seek-bar">
  6061. <div class="${ns}-seek-bar ${ns}-progress-bar" style="margin: 0 0.8rem;">
  6062. <div class="${ns}-full-bar">
  6063. <div class="${ns}-loaded-bar"></div>
  6064. <div class="${ns}-current-bar"></div>
  6065. </div>
  6066. </div>
  6067. </div>
  6068. <div class="${ns}-col-auto" data-hide-id="time" style="margin: 0 auto; padding: 0 0.25rem;">
  6069. <span class="${ns}-current-time">0:00</span> <span class="${ns}-duration" data-hide-id="duration">/0:00</span>
  6070. </div>
  6071. <div class="${ns}-col-auto" data-hide-id="volume-bar">
  6072. <div class="${ns}-volume-bar ${ns}-progress-bar" style="margin: 0 0.8rem;">
  6073. <div class="${ns}-full-bar">
  6074. <div class="${ns}-current-bar" style="width: ${Player.audio.volume * 100}%"></div>
  6075. </div>
  6076. </div>
  6077. </div>
  6078. <div class="${ns}-col-auto" data-hide-id="fullscreen" style="margin: 0 auto;">
  6079. <div class="${ns}-media-control ${ns}-fullscreen-button">
  6080. <div class="${ns}-fullscreen-button-display"></div>
  6081. </div>
  6082. </div>
  6083. <div class="${ns}-col-auto" style="padding: 0 0.25rem 0 0;"">
  6084. </div>`
  6085. }),
  6086. /* 21 - Templates
  6087. CSS */
  6088. (function(module, exports) {
  6089.  
  6090. module.exports = (data = {}) => `
  6091.  
  6092. /*
  6093. *
  6094. * CONTROLS CSS
  6095. *
  6096. */
  6097.  
  6098. .${ns}-controls {
  6099. align-items: center;
  6100. padding: 0.5rem 0;
  6101. position: relative;
  6102. justify-content: space-between;
  6103. background: ${Player.config.colors.controls_panel};
  6104. border-top: solid ${Player.config.borderWidth} ${Player.config.colors.border};
  6105. border-bottom: solid ${Player.config.borderWidth} ${Player.config.colors.border};
  6106. }
  6107. .${ns}-media-control {
  6108. height: 1.2rem;
  6109. width: 1.5rem;
  6110. display: flex;
  6111. justify-content: center;
  6112. align-items: center;
  6113. cursor: pointer
  6114. }
  6115. .${ns}-media-control .${ns}-col-auto {
  6116. padding: 0 0.5rem;
  6117. }
  6118. .${ns}-media-control>div {
  6119. height: 1rem;
  6120. width: .8rem;
  6121. background: ${Player.config.colors.buttons_color};
  6122. }
  6123. .${ns}-media-control:hover>div {
  6124. background: ${Player.config.colors.hover_color};
  6125. }
  6126. .${ns}-play-button-display {
  6127. clip-path: polygon(10% 10%, 10% 90%, 35% 90%, 35% 10%, 65% 10%, 65% 90%, 90% 90%, 90% 10%, 10% 10%)
  6128. }
  6129. .${ns}-play-button-display.${ns}-play {
  6130. clip-path: polygon(0 0, 0 100%, 100% 50%, 0 0)
  6131. }
  6132. .${ns}-previous-button-display,
  6133. .${ns}-next-button-display {
  6134. clip-path: polygon(10% 10%, 10% 90%, 30% 90%, 30% 50%, 90% 90%, 90% 10%, 30% 50%, 30% 10%, 10% 10%)
  6135. }
  6136. .${ns}-next-button-display {
  6137. transform: scale(-1, 1)
  6138. }
  6139. .${ns}-fullscreen-button-display {
  6140. width: 1rem !important;
  6141. clip-path: polygon(0% 35%, 0% 0%, 35% 0%, 35% 15%, 15% 15%, 15% 35%, 0% 35%, 0% 100%, 35% 100%, 35% 85%, 15% 85%, 15% 65%, 0% 65%, 100% 65%, 100% 100%, 65% 100%, 65% 85%, 85% 85%, 85% 15%, 65% 15%, 65% 0%, 100% 0%, 100% 35%, 85% 35%, 85% 65%, 0% 65%)
  6142. }
  6143. .${ns}-controls .${ns}-current-time {
  6144. font-size: 14px;
  6145. color: ${Player.config.colors.controls_current_time};
  6146. }
  6147. .${ns}-duration {
  6148. font-size: 14px;
  6149. color: ${Player.config.colors.controls_duration};
  6150. }
  6151. .${ns}-progress-bar {
  6152. min-width: 3.5rem;
  6153. height: 1.2rem;
  6154. display: flex;
  6155. align-items: center;
  6156. }
  6157. .${ns}-progress-bar .${ns}-full-bar {
  6158. height: .3rem;
  6159. width: 100%;
  6160. background: ${Player.config.colors.progress_bar};
  6161. border-radius: 1rem;
  6162. position: relative
  6163. }
  6164. .${ns}-progress-bar .${ns}-full-bar>div {
  6165. position: absolute;
  6166. top: 0;
  6167. bottom: 0;
  6168. border-radius: 1rem
  6169. }
  6170. .${ns}-progress-bar .${ns}-full-bar .${ns}-loaded-bar {
  6171. background: ${Player.config.colors.progress_bar_loaded};
  6172. }
  6173. .${ns}-progress-bar .${ns}-full-bar .${ns}-current-bar {
  6174. background: ${Player.config.colors.buttons_color};
  6175. display: flex;
  6176. justify-content: flex-end;
  6177. align-items: center
  6178. }
  6179. .${ns}-progress-bar .${ns}-full-bar .${ns}-current-bar:after {
  6180. content: "";
  6181. background: ${Player.config.colors.buttons_color};
  6182. height: .8rem;
  6183. min-width: .8rem;
  6184. border-radius: 1rem;
  6185. box-shadow: rgba(0, 0, 0, .76) 0 0 3px 0
  6186. }
  6187. .${ns}-progress-bar:hover .${ns}-current-bar:after {
  6188. background: ${Player.config.colors.hover_color};
  6189. }
  6190. .${ns}-seek-bar .${ns}-current-bar {
  6191. background: ${Player.config.colors.hover_color};
  6192. }
  6193. .${ns}-volume-bar .${ns}-current-bar {
  6194. background: ${Player.config.colors.controls_current_time};
  6195. }
  6196. .${ns}-chan-x-controls {
  6197. align-items: inherit
  6198. }
  6199. .${ns}-chan-x-controls .${ns}-current-time,
  6200. .${ns}-chan-x-controls .${ns}-duration {
  6201. margin: 0 .25rem
  6202. }
  6203. .${ns}-chan-x-controls .${ns}-media-control {
  6204. width: 1rem;
  6205. height: auto;
  6206. margin-top: -1px
  6207. }
  6208. .${ns}-chan-x-controls .${ns}-media-control>div {
  6209. height: .7rem;
  6210. width: .5rem
  6211. }
  6212.  
  6213. /*
  6214. *
  6215. * FOOTER CSS
  6216. *
  6217. */
  6218.  
  6219. .${ns}-footer {
  6220. padding: .15rem .25rem;
  6221. border-top: solid ${Player.config.borderWidth} ${Player.config.colors.border};
  6222. font-size: 13px;
  6223. }
  6224. .${ns}-footer .${ns}-expander {
  6225. position: absolute;
  6226. bottom: 0px;
  6227. right: 0px;
  6228. height: .75rem;
  6229. width: .75rem;
  6230. cursor: se-resize;
  6231. background:linear-gradient(to bottom right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 65%, ${Player.config.colors.buttons_color} 65%, ${Player.config.colors.buttons_color} 100%)
  6232. }
  6233. .${ns}-footer .${ns}-expander:hover {
  6234. background:linear-gradient(to bottom right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 65%, ${Player.config.colors.hover_color} 65%, ${Player.config.colors.hover_color} 100%)
  6235. }
  6236. .${ns}-footer:hover .${ns}-hover-display {
  6237. display: inline-block
  6238. }
  6239. .${ns}-footer .${ns}-footer-right {
  6240. float: right;
  6241. margin-right: 0.25rem;
  6242. display: flex;
  6243. justify-content: center; /* Horizontal center */
  6244. /*align-items: center;*/ /* Vertical center */
  6245. text-align: center; /* Optional: center text inside the box */
  6246. }
  6247. .${ns}-footer .${ns}-footer-left {
  6248. float: left;
  6249. display: flex;
  6250. justify-content: center; /* Horizontal center */
  6251. /*align-items: center;*/ /* Vertical center */
  6252. text-align: center; /* Optional: center text inside the box */
  6253. }
  6254. /*
  6255. *
  6256. * HEADER CSS
  6257. *
  6258. */
  6259.  
  6260. .${ns}-header {
  6261. cursor: grab;
  6262. text-align: center;
  6263. border-bottom:solid ${Player.config.borderWidth} ${Player.config.colors.border};
  6264. padding: .25rem;
  6265. }
  6266. .${ns}-header:hover .${ns}-hover-display {
  6267. display: flex
  6268. }
  6269. .${ns}-header.${ns}-row .${ns}-col.${ns}-truncate-text {
  6270. display: flex;
  6271. justify-content: center; /* Horizontal center */
  6272. align-items: center; /* Vertical center */
  6273. text-align: center; /* Optional: center text inside the box */
  6274. font-size: calc(${Player.config.fontSize}px);
  6275. }
  6276. html.fourchan-x .fa-repeat.fa-repeat-one::after {
  6277. content: "1";
  6278. font-size: .5rem;
  6279. visibility: visible;
  6280. margin-left: -1px
  6281. }
  6282.  
  6283. /*
  6284. *
  6285. * UI CSS
  6286. *
  6287. */
  6288.  
  6289. .${ns}-ui-button {
  6290. color:${Player.config.colors.buttons_color} !important;
  6291. }
  6292. .${ns}-ui-button:hover {
  6293. color:${Player.config.colors.hover_color} !important;
  6294. }
  6295. .${ns}-ui-icon {
  6296. color:${Player.config.colors.text} !important;
  6297. }
  6298. .${ns}-ui-icon:hover {
  6299. color:${Player.config.colors.text} !important;
  6300. }
  6301.  
  6302. /*
  6303. *
  6304. * IMAGE CSS
  6305. *
  6306. */
  6307.  
  6308. #${ns}-container[data-view-style=fc-sounds-playing] .${ns}-view-container .${ns}-media:not(.${ns}-pip),
  6309. #${ns}-container[data-view-style=playlist] .${ns}-view-container .${ns}-media:not(.${ns}-pip),
  6310. #${ns}-container[data-view-style=gallery] .${ns}-view-container .${ns}-media:not(.${ns}-pip),
  6311. #${ns}-container[data-view-style=image] .${ns}-view-container .${ns}-media:not(.${ns}-pip) {
  6312. text-align: center;
  6313. display: flex;
  6314. justify-items: center;
  6315. justify-content: center;
  6316. position: relative;
  6317. resize: both;
  6318. overflow: hidden;
  6319. min-height: ${Player.config.minMediaHeight} !important;
  6320. max-height: ${Player.config.maxMediaHeight} !important;
  6321. min-width: 100%;
  6322. max-width: 100%;
  6323. }
  6324. #${ns}-container[data-view-style=fullscreen] .${ns}-view-container .${ns}-media:not(.${ns}-pip) {
  6325. text-align: center;
  6326. display: flex;
  6327. justify-items: center;
  6328. justify-content: center;
  6329. position: relative;
  6330. resize: both;
  6331. overflow: hidden;
  6332. min-width: 100%;
  6333. max-width: 100%;
  6334. }
  6335. .${ns}-media.${ns}-pip {
  6336. text-align: right;
  6337. position: fixed !important;
  6338. right: ${Player.config.offsetRightPIP} !important;
  6339. bottom: ${Player.config.offsetBottomPIP} !important;
  6340. left: auto !important;
  6341. top: auto !important;
  6342. z-index: ${Player.config.zIndexPIP};
  6343. }
  6344. .${ns}-media .${ns}-video {
  6345. display: none
  6346. }
  6347. .${ns}-image,
  6348. .${ns}-video {
  6349. object-fit: contain
  6350. }
  6351. #${ns}-container[data-view-style=fullscreen] .${ns}-image,
  6352. #${ns}-container[data-view-style=fullscreen] .${ns}-video {
  6353. height: 100% !important;
  6354. width: 100% !important;
  6355. max-height: 100% !important;
  6356. max-width: 100% !important;
  6357. }
  6358. .${ns}-media.${ns}-show-video .${ns}-video {
  6359. display: block
  6360. }
  6361. .${ns}-media.${ns}-show-video .${ns}-image {
  6362. display: none
  6363. }
  6364. .${ns}-media img,
  6365. .${ns}-media video {
  6366. object-fit: contain;
  6367. pointer-events: none; /* Disable clicks on the link */
  6368. }
  6369. .${ns}-resize-handle {
  6370. position: absolute;
  6371. right: 0;
  6372. bottom: 0;
  6373. width: 5px;
  6374. height: 5px;
  6375. cursor: se-resize;
  6376. /*z-index: 3;*/
  6377. }
  6378. .${ns}-image-link {
  6379. display: block;
  6380. position: absolute;
  6381. width: 80% !important;
  6382. height: 94% !important;
  6383. opacity: 0;
  6384. }
  6385. .${ns}-media.${ns}-pip .${ns}-image-link {
  6386. display: block;
  6387. position: absolute;
  6388. width: 100% !important;
  6389. height: 100% !important;
  6390. opacity: 0;
  6391. }
  6392.  
  6393. /*
  6394. *
  6395. * LAYOUT CSS
  6396. *
  6397. */
  6398.  
  6399. #${ns}-container {
  6400. position: fixed;
  6401. background:${Player.config.colors.background};
  6402. border: solid ${Player.config.borderWidth} ${Player.config.colors.border};
  6403. min-width: 179px;
  6404. width: 375px;
  6405. color:${Player.config.colors.text};
  6406. }
  6407. .${ns}-panel {
  6408. padding: 0 .25rem;
  6409. height: 100%;
  6410. width: calc(100% - .5rem);
  6411. overflow: auto
  6412. }
  6413. .${ns}-heading {
  6414. font-weight: 600;
  6415. margin: .5rem 0;
  6416. min-width: 100%
  6417. }
  6418. .${ns}-has-description {
  6419. cursor: help
  6420. }
  6421. .${ns}-heading-action {
  6422. font-weight: normal;
  6423. text-decoration: underline;
  6424. margin-left: .25rem
  6425. }
  6426. .${ns}-row {
  6427. display: flex;
  6428. flex-wrap: wrap;
  6429. min-width: 100%;
  6430. box-sizing: border-box
  6431. }
  6432. .${ns}-col-auto {
  6433. flex: 0 0 auto;
  6434. width: auto;
  6435. max-width: 100%;
  6436. display: inline-flex
  6437. }
  6438. .${ns}-col {
  6439. flex-basis: 0;
  6440. flex-grow: 1;
  6441. max-width: 100%;
  6442. width: 100%
  6443. }
  6444. html.fourchan-x #${ns}-container .icon {
  6445. font-size: 0;
  6446. visibility: hidden;
  6447. margin: 0 .15rem
  6448. }
  6449. .${ns}-truncate-text {
  6450. white-space: nowrap;
  6451. text-overflow: clip;
  6452. overflow: hidden
  6453. }
  6454. .${ns}-hover-display {
  6455. display: none
  6456. }
  6457.  
  6458. /*
  6459. *
  6460. * LIST CSS
  6461. *
  6462. */
  6463.  
  6464. .${ns}-player .${ns}-hover-image {
  6465. position: fixed;
  6466. max-height: 125px;
  6467. max-width: 125px
  6468. }
  6469. .${ns}-player.${ns}-hide-hover-image .${ns}-hover-image {
  6470. display: none !important
  6471. }
  6472. .${ns}-list-container {
  6473. overflow-y: auto;
  6474. scrollbar-color: ${Player.config.colors.controls_panel} ${Player.config.colors.background};
  6475. height: 200px;
  6476. font-size: calc(${Player.config.fontSize}px)
  6477. }
  6478. .${ns}-list-container .${ns}-list-item {
  6479. list-style-type: none;
  6480. padding: .15rem .25rem;
  6481. white-space: nowrap;
  6482. text-overflow: ellipsis;
  6483. cursor: pointer;
  6484. background:${Player.config.colors.odd_row};
  6485. overflow: hidden;
  6486. height: calc(${Player.config.fontSize * 0.1}rem);
  6487. font-size: calc(${Player.config.fontSize}px)
  6488. }
  6489. .${ns}-list-container .${ns}-list-item.playing {
  6490. background: ${Player.config.colors.playing} !important;
  6491. color: ${Player.config.colors.text_playing} !important
  6492. }
  6493. .${ns}-list-container .${ns}-list-item:nth-child(2n) {
  6494. background:${Player.config.colors.even_row}
  6495. }
  6496. .${ns}-list-container .${ns}-list-item .${ns}-item-menu-button {
  6497. right: .25rem
  6498. }
  6499. .${ns}-list-container .${ns}-list-item:hover .${ns}-hover-display {
  6500. display: flex
  6501. }
  6502. .${ns}-list-container .${ns}-list-item.${ns}-dragging {
  6503. background: ${Player.config.colors.dragging} !important;
  6504. color: ${Player.config.colors.text_playing} !important
  6505. }
  6506. html:not(.fourchan-x) .dialog {
  6507. background:${Player.config.colors.background};
  6508. background:${Player.config.colors.background};
  6509. border-color:${Player.config.colors.border};
  6510. border-radius: 3px;
  6511. box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
  6512. border-radius: 3px;
  6513. padding-top: 1px;
  6514. padding-bottom: 3px
  6515. }
  6516. html:not(.fourchan-x) .${ns}-item-menu .entry {
  6517. position: relative;
  6518. display: block;
  6519. padding: .125rem .5rem;
  6520. min-width: 70px;
  6521. white-space: nowrap
  6522. }
  6523. html:not(.fourchan-x) .${ns}-item-menu .has-submenu::after {
  6524. content: "";
  6525. border-left: .5em solid;
  6526. border-top: .3em solid transparent;
  6527. border-bottom: .3em solid transparent;
  6528. display: inline-block;
  6529. margin: .35em;
  6530. position: absolute;
  6531. right: 3px
  6532. }
  6533. html:not(.fourchan-x) .${ns}-item-menu .submenu {
  6534. position: absolute;
  6535. display: none
  6536. }
  6537. html:not(.fourchan-x) .${ns}-item-menu .focused>.submenu {
  6538. display: block
  6539. }
  6540. .${ns}-list-container .${ns}-list-item .${ns}-col.${ns}-truncate-text {
  6541. background: transparent !important
  6542. }
  6543. .${ns}-list-container .${ns}-list-item .${ns}-col.${ns}-truncate-text span {
  6544. background: transparent !important
  6545. }
  6546. .${ns}-playlist-file-ext {
  6547. display: inline-block;
  6548. min-width: calc(${Player.config.fontSize * 4}px);
  6549. text-align: left;
  6550. background: transparent !important;
  6551. }
  6552.  
  6553. /*
  6554. *
  6555. * SETTINGS CSS
  6556. *
  6557. */
  6558.  
  6559. .${ns}-settings textarea {
  6560. border: solid ${Player.config.borderWidth} ${Player.config.colors.border};
  6561. min-width: 100%;
  6562. min-height: 4rem;
  6563. box-sizing: border-box;
  6564. white-space: pre
  6565. }
  6566. .${ns}-settings .${ns}-sub-settings .${ns}-col {
  6567. min-height: 1.55rem;
  6568. display: flex;
  6569. align-items: center;
  6570. align-content: center;
  6571. white-space: nowrap
  6572. }
  6573. .${ns}-settings .${ns}-heading-action {
  6574. font-size: 12px;
  6575. }
  6576. .${ns}-settings .${ns}-col {
  6577. font-size: 16px;
  6578. }
  6579. .${ns}-settings .${ns}-col select {
  6580. font-size: 12px;
  6581. }
  6582. .${ns}-settings .${ns}-heading {
  6583. font-size: 19px;
  6584. }
  6585. .${ns}-settings .${ns}-heading::before {
  6586. content: "";
  6587. display: block;
  6588. border-top: solid ${Player.config.borderWidth};
  6589. opacity: 0.2;
  6590. margin-bottom: 0.7em;
  6591. width: 100%;
  6592. }
  6593.  
  6594. /*
  6595. *
  6596. * THREADS CSS
  6597. *
  6598. */
  6599.  
  6600. .${ns}-threads .${ns}-thread-board-list label {
  6601. display: inline-block;
  6602. width: 4rem
  6603. }
  6604. .${ns}-threads .${ns}-thread-list {
  6605. margin: 1rem -0.25rem 0;
  6606. padding: .5rem 1rem;
  6607. border-top:solid ${Player.config.borderWidth} ${Player.config.colors.border}
  6608. }
  6609. .${ns}-threads .${ns}-thread-list .boardBanner {
  6610. margin: 1rem 0
  6611. }
  6612. .${ns}-threads table {
  6613. margin-top: .5rem;
  6614. border-collapse: collapse
  6615. }
  6616. .${ns}-threads table th {
  6617. border-bottom:solid ${Player.config.borderWidth} ${Player.config.colors.border}
  6618. }
  6619. .${ns}-threads table th,
  6620. .${ns}-threads table td {
  6621. text-align: left;
  6622. padding: .25rem
  6623. }
  6624. .${ns}-threads table tr {
  6625. padding: .25rem 0
  6626. }
  6627. .${ns}-threads table .${ns}-threads-body tr {
  6628. background:${Player.config.colors.even_row}
  6629. }
  6630. .${ns}-threads table .${ns}-threads-body tr:nth-child(2n) {
  6631. background:${Player.config.colors.odd_row}
  6632. }
  6633. .${ns}-threads,
  6634. .${ns}-settings,
  6635. .${ns}-player {
  6636. display: none
  6637. }
  6638. #${ns}-container[data-view-style=settings] .${ns}-settings {
  6639. display: block
  6640. }
  6641. #${ns}-container[data-view-style=threads] .${ns}-threads {
  6642. display: block
  6643. }
  6644. #${ns}-container[data-view-style=image] .${ns}-player,
  6645. #${ns}-container[data-view-style=playlist] .${ns}-player,
  6646. #${ns}-container[data-view-style=gallery] .${ns}-player,
  6647. #${ns}-container[data-view-style=fullscreen] .${ns}-player {
  6648. display: block
  6649. }
  6650. #${ns}-container[data-view-style=image] .${ns}-list-container {
  6651. display: none
  6652. }
  6653. #${ns}-container[data-view-style=image] .${ns}-media {
  6654. height: auto
  6655. }
  6656. #${ns}-container[data-view-style=playlist] .${ns}-media,
  6657. #${ns}-container[data-view-style=gallery] .${ns}-media {
  6658. height: 125px
  6659. }
  6660. #${ns}-container[data-view-style=fullscreen] .${ns}-media {
  6661. height: calc(100% - .4rem) !important
  6662. }
  6663. #${ns}-container[data-view-style=fullscreen] .${ns}-controls {
  6664. position: absolute;
  6665. left: 0;
  6666. right: 0;
  6667. bottom: calc(-2.5rem + .4rem)
  6668. }
  6669. #${ns}-container[data-view-style=fullscreen] .${ns}-controls:hover {
  6670. bottom: 0
  6671. }
  6672.  
  6673. /*
  6674. *
  6675. * GALLERY CSS
  6676. *
  6677. */
  6678.  
  6679. .${ns}-gallery-container {
  6680. display: grid;
  6681. grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  6682. grid-auto-rows: 80px;
  6683. gap: 8px;
  6684. padding: 8px;
  6685. overflow-y: auto;
  6686. scrollbar-color: ${Player.config.colors.controls_panel} ${Player.config.colors.background};
  6687. height: 100%;
  6688. box-sizing: border-box;
  6689. }
  6690.  
  6691. .${ns}-gallery-item {
  6692. position: relative;
  6693. height: 100%; /* Take full height of grid cell */
  6694. width: 100%; /* Take full width of grid cell */
  6695. min-height: 0; /* Critical for grid item sizing */
  6696. min-width: 0; /* Critical for grid item sizing */
  6697. box-sizing: border-box;
  6698. -moz-box-sizing: border-box;
  6699. -webkit-box-sizing: border-box;
  6700. border: 1.75px solid ${Player.config.colors.border};
  6701. border-radius: 3px;
  6702. overflow: hidden;
  6703. cursor: pointer;
  6704. background: ${Player.config.colors.odd_row};
  6705. }
  6706.  
  6707. .${ns}-gallery-item:nth-child(2n) {
  6708. background: ${Player.config.colors.odd_row};
  6709. }
  6710.  
  6711. .${ns}-gallery-item .${ns}-item-menu-button {
  6712. ;
  6713. }
  6714.  
  6715. .${ns}-gallery-thumb-container .${ns}-hover-display {
  6716. text-align: right;
  6717. position: absolute;
  6718. top: 0;
  6719. right: 0;
  6720. bottom: auto;
  6721. left: auto;
  6722. display: none;
  6723. font-size: 12px;
  6724. background: ${Player.config.colors.odd_row};
  6725. border-bottom: solid 1.75px ${Player.config.colors.border};
  6726. border-left: solid 1.75px ${Player.config.colors.border};
  6727. border-radius: 0 0 0 8px;
  6728. }
  6729.  
  6730. .${ns}-gallery-item.playing .${ns}-gallery-thumb-container .${ns}-hover-display {
  6731. background: ${Player.config.colors.progress_bar_loaded};
  6732. border-bottom: solid 3px ${Player.config.colors.buttons_color};
  6733. border-left: solid 3px ${Player.config.colors.buttons_color};
  6734. }
  6735.  
  6736. .${ns}-gallery-thumb-container:hover .${ns}-hover-display {
  6737. display: block
  6738. }
  6739.  
  6740. .${ns}-gallery-thumb-container {
  6741. width: 100%;
  6742. height: 100%;
  6743. min-height: 0;
  6744. display: flex;
  6745. align-items: center;
  6746. justify-content: center;
  6747. }
  6748.  
  6749. .${ns}-gallery-thumb {
  6750. max-height: 80%;
  6751. object-fit: contain;
  6752. bottom: 16px;
  6753. position: absolute;
  6754. /*overflow: hidden;*/
  6755. }
  6756.  
  6757. .${ns}-gallery-overlay-top {
  6758. position: absolute;
  6759. top: 0;
  6760. left: 0;
  6761. right: 0;
  6762. background: rgba(0, 0, 0, 0.7);
  6763. color: white;
  6764. padding: 2px 4px 3px 4px;
  6765. font-size: 10px;
  6766. text-align: center;
  6767. white-space: nowrap;
  6768. overflow: hidden;
  6769. text-overflow: ellipsis;
  6770. display: none;
  6771. }
  6772.  
  6773. .${ns}-gallery-overlay-bottom {
  6774. position: absolute;
  6775. bottom: 0;
  6776. left: 0;
  6777. right: 0;
  6778. background: rgba(0, 0, 0, 0.65);
  6779. color: white;
  6780. padding: 3px 4px 2px 4px;
  6781. font-size: 10px;
  6782. text-align: center;
  6783. white-space: nowrap;
  6784. overflow: hidden;
  6785. text-overflow: ellipsis;
  6786. }
  6787.  
  6788. .${ns}-gallery-item.playing {
  6789. box-sizing: border-box;
  6790. -moz-box-sizing: border-box;
  6791. -webkit-box-sizing: border-box;
  6792. background: ${Player.config.colors.progress_bar_loaded} !important;
  6793. border: 3px solid ${Player.config.colors.buttons_color};
  6794. border-radius: 2px;
  6795. filter: drop-shadow(0 0 0.1rem ${Player.config.colors.buttons_color});
  6796. }
  6797.  
  6798. .${ns}-gallery-item.playing .${ns}-gallery-overlay-top {
  6799. box-sizing: border-box;
  6800. -moz-box-sizing: border-box;
  6801. -webkit-box-sizing: border-box;
  6802. padding: 1px 3px 2px 3px;
  6803. background: ${Player.config.colors.buttons_color};
  6804. color: ${Player.config.colors.background};
  6805. border-bottom: solid 1px ${Player.config.colors.buttons_color};
  6806. }
  6807.  
  6808. .${ns}-gallery-item.playing .${ns}-gallery-overlay-bottom {
  6809. box-sizing: border-box;
  6810. -moz-box-sizing: border-box;
  6811. -webkit-box-sizing: border-box;
  6812. padding: 2px 3px 1px 3px;
  6813. background: ${Player.config.colors.buttons_color};
  6814. color: ${Player.config.colors.background};
  6815. border-top: solid 1px ${Player.config.colors.buttons_color};
  6816. }
  6817.  
  6818. .${ns}-gallery-item.playing .${ns}-gallery-thumb-container {
  6819. background: ${Player.config.colors.progress_bar_loaded};
  6820. }
  6821.  
  6822. .${ns}-gallery-item.playing .${ns}-gallery-thumb {
  6823. bottom: 15px
  6824. }
  6825.  
  6826. #${ns}-container[data-view-style=gallery] .${ns}-gallery-container {
  6827. display: grid !important;
  6828. }
  6829.  
  6830. #${ns}-container[data-view-style=gallery] .${ns}-list-container {
  6831. display: none;
  6832. }
  6833.  
  6834. #${ns}-container[data-view-style=fc-sounds-playing] .${ns}-gallery-container,
  6835. #${ns}-container[data-view-style=playlist] .${ns}-gallery-container,
  6836. #${ns}-container[data-view-style=image] .${ns}-gallery-container {
  6837. display: none;
  6838. }
  6839. `
  6840. }),
  6841.  
  6842. /* 22 - Templates
  6843. Footer */
  6844. (function(module, exports) {
  6845.  
  6846. module.exports = (data = {}) => Player.userTemplate.build({
  6847. template: Player.config.footerTemplate,
  6848. sound: Player.playing
  6849. }) +
  6850. `<div class="${ns}-expander"></div>`
  6851.  
  6852. }),
  6853. /* 23 - Templates
  6854. Header */
  6855. (function(module, exports) {
  6856.  
  6857. module.exports = (data = {}) => Player.userTemplate.build({
  6858. template: Player.config.headerTemplate,
  6859. sound: Player.playing,
  6860. defaultName: '8chan Sounds',
  6861. outerClass: `${ns}-col-auto`
  6862. });
  6863.  
  6864.  
  6865. }),
  6866. /* 24 - Templates
  6867. Context menus */
  6868. (function(module, exports) {
  6869.  
  6870. module.exports = (data = {}) => `<div class="${ns}-item-menu dialog" id="menu" tabindex="0" data-type="post" style="position: fixed; top: ${data.y}px; left: ${data.x}px;">
  6871. <a class="${ns}-remove-link entry focused" href="javascript:;" data-id="${data.sound.id}">Remove</a>
  6872. ${data.sound.post ? `<a class="entry" href="#${(is4chan ? 'p' : '') + data.sound.post}">Show Post</a>` : ''}
  6873. <div class="entry has-submenu">
  6874. Open
  6875. <div class="dialog submenu" style="inset: 0px auto auto 100%;">
  6876. <a class="entry" href="${data.sound.image}" target="_blank">Image</a>
  6877. <a class="entry" href="${data.sound.src}" target="_blank">Sound</a>
  6878. </div>
  6879. </div>
  6880. <div class="entry has-submenu">
  6881. Download
  6882. <div class="dialog submenu" style="inset: 0px auto auto 100%;">
  6883. <a class="${ns}-download-link entry" href="javascript:;" data-src="${data.sound.image}" data-name="${data.sound.filename}">Image</a>
  6884. <a class="${ns}-download-link entry" href="javascript:;" data-src="${data.sound.src}">Sound</a>
  6885. </div>
  6886. </div>
  6887. <div class="entry has-submenu">
  6888. Filter
  6889. <div class="dialog submenu" style="inset: 0px auto auto 100%;">
  6890. ${data.sound.imageMD5 ? `<a class="${ns}-filter-link entry" href="javascript:;" data-filter="${data.sound.imageMD5}">Image</a>` : ''}
  6891. <a class="${ns}-filter-link entry" href="javascript:;" data-filter="${data.sound.src.replace(/^(https?\:)?\/\//, '')}">Sound</a>
  6892. </div>
  6893. </div>
  6894. </div>`
  6895.  
  6896.  
  6897. }),
  6898. /* 25 - Templates
  6899. Playlist items */
  6900. (function(module, exports) {
  6901.  
  6902. module.exports = (data = {}) => (data.sounds || Player.sounds).map(sound =>
  6903. `<div class="${ns}-list-item ${ns}-row ${sound.playing ? 'playing' : ''}" data-id="${sound.id}" draggable="true">
  6904. ${Player.userTemplate.build({
  6905. template: Player.config.rowTemplate,
  6906. sound,
  6907. outerClass: `${ns}-col-auto`
  6908. })}
  6909. </div>`
  6910. ).join('')
  6911.  
  6912. }),
  6913. /* 26 - Templates
  6914. Media display */
  6915. (function(module, exports) {
  6916.  
  6917. module.exports = (data = {}) => `<div class="${ns}-media-and-controls">
  6918. <div class="${ns}-media">
  6919. <a class="${ns}-image-link" target="_blank"></a>
  6920. <img class="${ns}-image"></img>
  6921. <video class="${ns}-video"></video>
  6922. </div>
  6923. <div class="${ns}-controls ${ns}-row">
  6924. ${Player.templates.controls(data)}
  6925. </div>
  6926. </div>
  6927. <div class="${ns}-list-container" style="height: 100px">
  6928. ${Player.templates.list(data)}
  6929. </div>
  6930. <div class="${ns}-gallery-container" style="height: 100px">
  6931.  
  6932. </div>
  6933. <img class="${ns}-hover-image" style="display: none">`
  6934.  
  6935. }),
  6936. /* 27 - Templates
  6937. Settings panel */
  6938. (function(module, exports, __webpack_require__) {
  6939.  
  6940. module.exports = (data = {}) => {
  6941. const settingsConfig = __webpack_require__(1);
  6942.  
  6943. let tpl = `
  6944. <div style="text-align: right; font-size: 10px; font-weight: 600; margin: .5rem 0; min-width: 100%"><b>Version</b>
  6945. <a href="https://greasyfork.org/en/scripts/533468-8chan-sounds-player" target="_blank">${VERSION}</a>
  6946. </div>
  6947. <div class="${ns}-heading">Encode / Decode URL</div>
  6948. <div class="${ns}-row">
  6949. <input type="text" class="${ns}-decoded-input ${ns}-col" placeholder="https://">
  6950. <input type="text" class="${ns}-encoded-input ${ns}-col" placeholder="https%3A%2F%2F">
  6951. </div>
  6952. `;
  6953.  
  6954. settingsConfig.forEach(function addSetting(setting) {
  6955. // Filter settings that aren't flagged to be displayed.
  6956. if (!setting.showInSettings && !(setting.settings || []).find(s => s.showInSettings)) {
  6957. return;
  6958. }
  6959. const desc = setting.description;
  6960.  
  6961. tpl += `
  6962. <div class="${ns}-row ${setting.isSubSetting ? `${ns}-sub-settings` : ''}">
  6963. <div class="${ns}-col ${!setting.isSubSetting ? `${ns}-heading` : ''} ${desc ? `${ns}-has-description` : ''}" ${desc ? `title="${desc.replace(/"/g, '&quot;')}"` : ''}>
  6964. ${setting.title}
  6965. ${(setting.actions || []).map(action => `<a href="javascript:;" class="${ns}-heading-action" data-handler="${action.handler}" data-property="${setting.property}">${action.title}</a>`)}
  6966. </div>`;
  6967.  
  6968. if (setting.settings) {
  6969. setting.settings.forEach(subSetting => addSetting({
  6970. ...setting,
  6971. actions: null,
  6972. settings: null,
  6973. description: null,
  6974. ...subSetting,
  6975. isSubSetting: true
  6976. }));
  6977. } else {
  6978.  
  6979. let value = _get(Player.config, setting.property, setting.default),
  6980. attrs = (setting.attrs || '') + (setting.class ? ` class="${setting.class}"` : '') + ` data-property="${setting.property}"`;
  6981.  
  6982. if (setting.format) {
  6983. value = _get(Player, setting.format)(value);
  6984. }
  6985. let type = typeof value;
  6986.  
  6987. if (setting.split) {
  6988. value = value.join(setting.split);
  6989. } else if (type === 'object') {
  6990. value = JSON.stringify(value, null, 4);
  6991. }
  6992.  
  6993. tpl += `
  6994. <div class="${ns}-col">
  6995. ${
  6996. type === 'boolean'
  6997. ? `<input type="checkbox" ${attrs} ${value ? 'checked' : ''}></input>`
  6998.  
  6999. : setting.showInSettings === 'textarea' || type === 'object'
  7000. ? `<textarea ${attrs}>${value}</textarea>`
  7001.  
  7002. : setting.options
  7003. ? `<select ${attrs}>
  7004. ${Object.keys(setting.options).map(k => `<option value="${k}" ${value === k ? 'selected' : ''}>
  7005. ${setting.options[k]}
  7006. </option>`).join('')}
  7007. </select>`
  7008.  
  7009. : `<input type="text" ${attrs} value="${value}"></input>`
  7010. }
  7011. </div>`;
  7012. }
  7013. tpl += '</div>';
  7014. });
  7015.  
  7016. return tpl;
  7017. }
  7018.  
  7019. }),
  7020. /* 28 - Templates
  7021. Thread browser */
  7022. (function(module, exports) {
  7023.  
  7024. module.exports = (data = {}) => `<div class="${ns}-heading ${ns}-has-description" title="Search for threads with a sound OP">
  7025. Active Threads
  7026. ${!Player.threads.loading ? `- <a class="${ns}-fetch-threads-link ${ns}-heading-action" href="javascript:;">Update</a>` : ''}
  7027. </div>
  7028. <div style="display: ${Player.threads.loading ? 'block' : 'none'}">Loading</div>
  7029. <div style="display: ${Player.threads.loading ? 'none' : 'block'}">
  7030. <div class="${ns}-heading ${ns}-has-description" title="Only includes threads containing the search.">
  7031. Filter
  7032. </div>
  7033. <input type="text" class="${ns}-threads-filter" value="${Player.threads.filterValue || ''}"></input>
  7034. <div class="${ns}-heading">
  7035. Boards - <a class="${ns}-all-boards-link ${ns}-heading-action" href="javascript:;">${Player.threads.showAllBoards ? 'Selected Only' : 'Show All'}</a>
  7036. </div>
  7037. <div class="${ns}-thread-board-list">
  7038. ${Player.templates.threadBoards(data)}
  7039. </div>
  7040. ${
  7041. !Player.threads.hasParser || Player.config.threadsViewStyle === 'table'
  7042. ? `<table style="width: 100%">
  7043. <tr>
  7044. <th>Thread</th>
  7045. <th>Subject</th>
  7046. <th>Replies/Images</th>
  7047. <th>Started</th>
  7048. <th>Updated</th>
  7049. <tr>
  7050. <tbody class="${ns}-threads-body"></tbody>
  7051. </table>`
  7052. : `<div class="${ns}-thread-list"></div>`
  7053. }
  7054. </div>`
  7055.  
  7056.  
  7057. }),
  7058. /* 29 - Templates
  7059. Thread browser */
  7060. (function(module, exports) {
  7061.  
  7062. module.exports = (data = {}) => (Player.threads.boardList || []).map(board => {
  7063. let checked = Player.threads.selectedBoards.includes(board.board);
  7064. return !checked && !Player.threads.showAllBoards ?
  7065. '' :
  7066. `<label>
  7067. <input type="checkbox" value="${board.board}" ${checked ? 'checked' : ''}>
  7068. /${board.board}/
  7069. </label>`
  7070. }).join('')
  7071.  
  7072. }),
  7073. /* 30 - Templates
  7074. Thread browser */
  7075. (function(module, exports) {
  7076.  
  7077. module.exports = (data = {}) => Object.keys(Player.threads.displayThreads).reduce((rows, board) => {
  7078. return rows.concat(Player.threads.displayThreads[board].map(thread => `
  7079. <tr>
  7080. <td>
  7081. <a class="quotelink" href="//boards.${thread.ws_board ? '4channel' : '4chan'}.org/${thread.board}/thread/${thread.no}#p${thread.no}" target="_blank">
  7082. >>>/${thread.board}/${thread.no}
  7083. </a>
  7084. </td>
  7085. <td>${thread.sub || ''}</td>
  7086. <td>${thread.replies} / ${thread.images}</td>
  7087. <td>${timeAgo(thread.time)}</td>
  7088. <td>${timeAgo(thread.last_modified)}</td>
  7089. </tr>
  7090. `))
  7091. }, []).join('')
  7092.  
  7093.  
  7094. }),
  7095. /* 31 - Templates
  7096. Gallery playlist items */
  7097. (function(module, exports) {
  7098.  
  7099. module.exports = (data = {}) => (data.sounds || Player.sounds).map(sound =>
  7100. `<div class="${ns}-gallery-item ${Player.playing && Player.playing.id === sound.id ? 'playing' : ''}" data-id="${sound.id}" draggable="true" title="${sound.filename}">
  7101. <div class="${ns}-gallery-thumb-container">
  7102. <img class="${ns}-gallery-thumb" src="${sound.thumb}" loading="lazy" draggable="false">
  7103. <div class="${ns}-gallery-overlay-top">
  7104. <span class="${ns}-gallery-title">${sound.post} ${sound.fileSize}</span>
  7105. </div>
  7106. <div class="${ns}-gallery-overlay-bottom">
  7107. <span class="${ns}-gallery-title">${sound.post} ${sound.fileSize}</span>
  7108. </div>
  7109. <span class="${ns}-hover-display">
  7110. <a data-id="${sound.id}" href="javascript:;" class="fc-sounds-ui-button fc-sounds-item-menu-button fc-sounds-col-auto" draggable="false"> ▼ </a>
  7111. </span>
  7112. </div>
  7113. </div>
  7114. `).join('')
  7115. })
  7116. ]);