8chan sounds player

Play that faggy music weeb boi

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