您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Play that faggy music weeb boi
- // ==UserScript==
- // @name 8chan sounds player
- // @version 2.3.0_0027
- // @namespace 8chanss
- // @description Play that faggy music weeb boi
- // @author original by: RCC; ported to 8chan by: soundboy_1459944
- // @website https://greasyfork.org/en/scripts/533468-8chan-sounds-player
- // @match https://8chan.moe/*/res/*
- // @match https://8chan.se/*/res/*
- // @match https://8chan.moe/*/last/*
- // @match https://8chan.se/*/last/*
- // @connect 4chan.org
- // @connect 4channel.org
- // @connect a.4cdn.org
- // @connect 8chan.moe
- // @connect 8chan.se
- // @connect desu-usergeneratedcontent.xyz
- // @connect arch-img.b4k.co
- // @connect archive-media-0.nyafuu.org
- // @connect 4cdn.org
- // @connect a.pomf.cat
- // @connect pomf.cat
- // @connect litter.catbox.moe
- // @connect files.catbox.moe
- // @connect catbox.moe
- // @connect share.dmca.gripe
- // @connect z.zz.ht
- // @connect z.zz.fo
- // @connect zz.ht
- // @connect too.lewd.se
- // @connect lewd.se
- // @connect *
- // @grant GM.getValue
- // @grant GM.setValue
- // @grant GM.xmlHttpRequest
- // @grant GM_addValueChangeListener
- // @grant GM_getResourceURL
- // @grant GM_addElement
- // @run-at document-start
- // @license CC0 1.0
- // @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
- // ==/UserScript==
- //kudos to the original sound player by RCC: https://github.com/rcc11/4chan-sounds-player
- (function(modules) { // webpackBootstrap
- 'use strict';
- // The module cache
- var installedModules = {};
- // The require function
- function __webpack_require__(moduleId) {
- // Check if module is in cache
- if (installedModules[moduleId]) {
- return installedModules[moduleId].exports;
- }
- // Create a new module (and put it into the cache)
- var module = installedModules[moduleId] = {
- i: moduleId,
- l: false,
- exports: {}
- };
- // Execute the module function
- modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
- // Flag the module as loaded
- module.l = true;
- // Return the exports of the module
- return module.exports;
- }
- // expose the modules object (__webpack_modules__)
- __webpack_require__.m = modules;
- // expose the module cache
- __webpack_require__.c = installedModules;
- // define getter function for harmony exports
- __webpack_require__.d = function(exports, name, getter) {
- if (!__webpack_require__.o(exports, name)) {
- Object.defineProperty(exports, name, {
- enumerable: true,
- get: getter
- });
- }
- };
- // define __esModule on exports
- __webpack_require__.r = function(exports) {
- if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
- Object.defineProperty(exports, Symbol.toStringTag, {
- value: 'Module'
- });
- }
- Object.defineProperty(exports, '__esModule', {
- value: true
- });
- };
- // create a fake namespace object
- // mode & 1: value is a module id, require it
- // mode & 2: merge all properties of value into the ns
- // mode & 4: return value when already ns object
- // mode & 8|1: behave like require
- __webpack_require__.t = function(value, mode) {
- if (mode & 1) value = __webpack_require__(value);
- if (mode & 8) return value;
- if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
- var ns = Object.create(null);
- __webpack_require__.r(ns);
- Object.defineProperty(ns, 'default', {
- enumerable: true,
- value: value
- });
- if (mode & 2 && typeof value != 'string')
- for (var key in value) __webpack_require__.d(ns, key, function(key) {
- return value[key];
- }.bind(null, key));
- return ns;
- };
- // getDefaultExport function for compatibility with non-harmony modules
- __webpack_require__.n = function(module) {
- var getter = module && module.__esModule ?
- function getDefault() {
- return module['default'];
- } :
- function getModuleExports() {
- return module;
- };
- __webpack_require__.d(getter, 'a', getter);
- return getter;
- };
- // Object.prototype.hasOwnProperty.call
- __webpack_require__.o = function(object, property) {
- return Object.prototype.hasOwnProperty.call(object, property);
- };
- // __webpack_public_path__
- __webpack_require__.p = "";
- // Load entry module and return exports
- return __webpack_require__(__webpack_require__.s = 3);
- })
- ([
- /* 0 - File Parser
- • parseFileName(): Extracts sound URLs from filenames using regex pattern [sound=URL]
- • parsePost(): Processes individual posts to find sound files and create play buttons
- • parseFiles(): Scans the page or specific elements for posts containing sounds
- • Key Features:
- o Handles URL decoding
- o Creates unique IDs for each sound
- o Generates play links next to sound files
- */
- (function(module, exports) {
- const protocolRE = /^(https?:)?\/\//;
- const filenameRE = /(.*?)[[({](?:sound)[ =:|$](.*?)[\])}]/g;
- const videoFileExtRE = /\.(webm|mp4|m4v|ogv|avi|mpeg|mpg|mpe|m1v|m2v|mov|wmv)$/i;
- const audioFileExtRE = /\.(mp3|m4a|m4b|flac|ogg|oga|opus|mp2|mpega|wav|aac)$/i;
- const imageMimeRE = /^image\/.+$/;
- const videoMimeRE = /^video\/.+$/;
- const audioMimeRE = /^audio\/.+$/;
- //const playlistExtRE = /\.(m3u|asx)$/i;
- // Function to safely get file extension (handles multiple dots in filename)
- function getFileExtension(filename) {
- // Handle edge cases: no extension, hidden files, or filenames ending with dot
- if (!filename || filename.indexOf('.') === -1 || filename.endsWith('.')) {
- return '';
- }
- return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase();
- }
- function determinateMimeType(extension, isVideo, isAudio) {
- let type;
- if (isVideo) {
- switch (extension) {
- case 'webm': type = 'video/webm'; break;
- case 'mp4': type = 'video/mp4'; break;
- case 'm4v': type = 'video/mp4'; break;
- case 'ogv': type = 'video/ogg'; break;
- case 'avi': type = 'video/x-msvideo'; break;
- case 'mpeg': case 'mpg': case 'mpe': case 'm1v': case 'm2v':
- type = 'video/mpeg'; break;
- default: type = 'video/mp4'; // default fallback
- }
- } else if (isAudio) {
- switch (extension) {
- case 'mp3': case 'mpega': case 'mp2': type = 'audio/mpeg'; break;
- case 'm4a': case 'm4b': type = 'audio/mp4'; break;
- case 'flac': type = 'audio/flac'; break;
- case 'ogg': case 'oga': case 'opus': type = 'audio/ogg'; break;
- case 'wav': type = 'audio/wav'; break;
- case 'aac': type = 'audio/aac'; break;
- default: type = 'audio/mpeg'; // default fallback
- }
- } else {
- type = 'audio/mpeg'; // ultimate fallback
- }
- return type;
- }
- function getFullFilename(element) {
- if (element.dataset.fileExt) {
- return element.textContent + element.nextElementSibling.textContent;
- }
- return element.textContent;
- }
- function formatFileTitle(postId, fileIndex, fileSize, filename) {
- // Extract file extension
- const fileExt = filename.split('.').pop().toLowerCase();
- // Get base filename without extension
- let baseName = filename.replace(/\.[^/.]+$/, "");
- 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.
- // local files case (module 13 addFromFiles())
- if(fileSize == null) return `localFile:${localFileCounter} <span class="${ns}-playlist-file-ext">.${fileExt}</span> ${baseName}`;
- const displaySize = formatFileSize(fileSize);
- return `${postId} ${displaySize} <span class="${ns}-playlist-file-ext">.${fileExt}</span> ${baseName}`;
- }
- function formatFileSize(fileSize) {
- // local files case (module 13 addFromFiles())
- if(fileSize == null) return 'NULL';
- // Convert fileSize (assumed to be a string like "99.50 KB" or "1.82 MB") into MB
- let sizeValue = parseFloat(fileSize);
- let sizeInMB = 0;
- if (fileSize.toLowerCase().includes("kb")) {
- sizeInMB = sizeValue / 1024;
- } else if (fileSize.toLowerCase().includes("mb")) {
- sizeInMB = sizeValue;
- }
- // Round up to 1 decimal place
- sizeInMB = Math.ceil(sizeInMB * 10) / 10;
- // Cap anything over 99.5 MB
- // Omit the .0 when it's a 2 digits number like 11.0 MB (11.0 MB → 11 MB).
- const displaySize = sizeInMB > 99.5 ? "99+ MB" : `${(sizeInMB > 9.9 ? ' ' + sizeInMB.toFixed(0): sizeInMB.toFixed(1))} MB`;
- return displaySize;
- }
- function getPostNumber(postElement) {
- // If not found in ID, look for the linkQuote element
- const linkQuote = postElement.querySelector('.linkQuote');
- if (linkQuote && linkQuote.textContent && /^\d+$/.test(linkQuote.textContent)) {
- return linkQuote.textContent;
- }
- // Fallback to a generated ID if nothing else works
- return 'idGrabFailed';
- }
- function parseFileName(filename, image, post, thumb, imageMD5, fileIndex, fileSize, dataFilemime) {
- if (!filename) return [];
- filename = filename.replace(/-/, '/');
- // First check for [sound=URL] tags
- const matches = [];
- let match;
- while ((match = filenameRE.exec(filename)) !== null) {
- matches.push(match);
- }
- // If we found sound tags, process them and ignore video files
- if (matches.length) {
- return matches.reduce((sounds, match, i) => {
- let src = match[2];
- const id = post + ':' + fileIndex;
- //const title = match[1].trim() || defaultName + (matches.length > 1 ? ` (${i + 1})` : '');
- try {
- if (src.includes('_') && !src.includes('%')) src = src.replace(/_/g, '%'); // Fix for Firefox issue: replace underscores that were mistakenly used instead of percent-encoding
- if (src.includes('%')) src = decodeURIComponent(src);
- if (src.match(protocolRE) === null) src = (location.protocol + '//' + src);
- } catch (error) {
- return sounds;
- }
- // Determine if this is a video file based on extension
- const isVideo = videoFileExtRE.test(src) ? true : false;
- const isAudio = audioFileExtRE.test(src) ? true : false;
- // Determine the MIME type based on extension
- const extension = getFileExtension(src);
- let type = determinateMimeType(extension, isVideo, isAudio)
- const sound = {
- src, // external sound
- id,
- title: formatFileTitle(post, fileIndex, fileSize, filename),
- post,
- image, // image or video taked from the post
- filename,
- thumb,
- imageMD5,
- type, // external sound
- isVideo, // is external sound video?
- hasSoundTag: true,
- fileIndex: fileIndex,
- fileSize: formatFileSize(fileSize),
- dataFilemime: dataFilemime
- };
- Player.acceptedSound(sound) && sounds.push(sound);
- return sounds;
- }, []);
- }
- // If no sound tags found, check for video files
- const isVideo = videoMimeRE.test(dataFilemime);
- const isAudio = audioMimeRE.test(dataFilemime);
- if (isVideo || isAudio) {
- const id = post + ':' + fileIndex + ':0';
- // Determine the MIME type based on extension
- const extension = getFileExtension(image);
- let type = determinateMimeType(extension, isVideo, false)
- return [{
- src: image, // Use the image URL as src for video files
- id: post + ':' + fileIndex,
- title: formatFileTitle(post, fileIndex, fileSize, filename),
- post,
- image, // image (post file)
- filename,
- thumb,
- imageMD5,
- type, // image (post file)
- isVideo, // is image (post file) video?
- hasSoundTag: false, // external sound
- fileIndex: fileIndex,
- fileSize: formatFileSize(fileSize),
- dataFilemime: dataFilemime
- }];
- }
- return [];
- }
- function parsePost(post, skipRender) {
- try {
- // Get the actual post number for this post
- const postNumber = getPostNumber(post);
- if (!postNumber) return;
- // If there are existing play links, just reconnect their handlers
- const existingLinks = post.querySelectorAll(`.${ns}-play-link`);
- if (existingLinks.length > 0) {
- existingLinks.forEach(link => {
- const id = link.getAttribute('data-id');
- link.onclick = () => Player.play(Player.sounds.find(sound => sound.id === id));
- });
- return;
- }
- // Get all file containers in the post
- const fileContainers = post.querySelectorAll('.uploadCell');
- if (!fileContainers || fileContainers.length === 0) return;
- let allSounds = [];
- // Process each file in the post
- fileContainers.forEach((container, fileIndex) => {
- let filename = null;
- let fileLink = null;
- let fileSize = "0 KB";
- // Try to get filename from various locations
- const originalNameLink = container.querySelector('.originalNameLink');
- if (originalNameLink) filename = getFullFilename(originalNameLink);
- // Get file size if available
- const sizeLabel = container.querySelector('.sizeLabel');
- if (sizeLabel) fileSize = sizeLabel.textContent.trim();
- // Get file dimensions if available
- const dimensionLabel = container.querySelector('.dimensionLabel'); // e.g. '123x123'
- // If no filename found via standard selectors, try to find file links
- if (!filename) {
- const fileLinkEl = container.querySelector('.nameLink');
- if (fileLinkEl) {
- fileLink = fileLinkEl.href;
- filename = fileLink.split('/').pop();
- }
- }
- if (!filename) return;
- const fileThumb = container.querySelector('.imgLink');
- const imageSrc = fileThumb && fileThumb.href;
- const thumbImg = fileThumb && fileThumb.querySelector('img');
- let thumbSrc = thumbImg && thumbImg.src;
- const md5Match = imageSrc && imageSrc.match(/\/\.media\/([a-f0-9]{64})/i);
- const imageMD5 = md5Match && md5Match[1];
- const dataFilemime = fileThumb && fileThumb.getAttribute('data-filemime');
- // Replace spoiler thumbnail with actual thumbnail if available
- const regex = /custom\.spoiler$|spoiler\.png$/;
- if (regex.test(thumbImg.src)) {
- const domain = new URL(thumbImg.src).origin;
- thumbSrc = imageSrc && `${domain}/.media/t_${imageMD5}`;
- }
- // set full image as a thumbnail for small images (220x220 or less). fix for small images. small images don't have thumbnails.
- if (dimensionLabel && imageMimeRE.test(dataFilemime)) {
- const dimensions = dimensionLabel.textContent.trim().split('x');
- if (dimensions.length === 2) {
- const width = parseInt(dimensions[0]);
- const height = parseInt(dimensions[1]);
- if (width <= 220 && height <= 220) {
- thumbSrc = imageSrc;
- }
- }
- }
- const sounds = parseFileName(filename, imageSrc, postNumber, thumbSrc, imageMD5, fileIndex, fileSize, dataFilemime);
- if (!sounds.length) return;
- allSounds = allSounds.concat(sounds);
- // Create play link for this file
- const firstID = sounds[0].id;
- const text = '▶︎';
- const clss = `${ns}-play-link`;
- let playLinkParent = container.querySelector('.uploadDetails') ||
- container.querySelector('.fileLink') ||
- container.querySelector('.fileText') ||
- container; // Fallback to the container itself
- if (playLinkParent) {
- const playLink = document.createElement('a');
- playLink.href = "javascript:;";
- playLink.className = clss;
- playLink.setAttribute('data-id', firstID);
- playLink.textContent = text;
- playLink.title = 'play';
- playLink.style.display = 'inline-block'; // Ensure the link is displayed inline
- playLink.style.marginLeft = '3px'; // Add some spacing
- playLink.onclick = () => Player.play(Player.sounds.find(sound => sound.id === firstID));
- playLinkParent.appendChild(document.createTextNode(' '));
- playLinkParent.appendChild(playLink);
- }
- });
- if (allSounds.length === 0) return;
- allSounds.forEach(sound => Player.add(sound, skipRender));
- return allSounds.length > 0;
- } catch (err) {
- console.error('[8chan sounds player] Error parsing post:', err);
- }
- }
- function parseFiles(target, postRender) {
- let addedSounds = false;
- let posts = target.classList && target.classList.contains('postCell') ?
- [target] :
- target.querySelectorAll('.innerOP, .innerPost');
- posts.forEach(post => parsePost(post, postRender) && (addedSounds = true));
- if (addedSounds && postRender && Player.container) Player.playlist.render();
- }
- module.exports = {
- parseFiles,
- parsePost,
- parseFileName
- };
- }),
- /* 1 - Settings Configuration
- • Contains all default configuration options for the player:
- o Playback settings (shuffle, repeat)
- o UI settings (view styles, hover images)
- o Keybindings
- o Allowed hosts list
- o Color schemes
- o Template layouts
- • Defines the structure for:
- o Header/footer/row templates
- o Hotkey bindings
- o Player appearance settings
- */
- (function(module, exports) {
- module.exports = [{
- property: 'shuffle',
- default: false
- },
- {
- property: 'repeat',
- default: 'all'
- },
- {
- property: 'viewStyle',
- default: 'playlist'
- },
- {
- property: 'hoverImages',
- default: false
- },
- {
- property: 'preventHoverImagesFor',
- default: [],
- save: false
- },
- {
- title: 'Miscellaneous',
- description: 'Variety of different settings',
- showInSettings: true,
- settings: [{
- property: 'fontSize',
- title: 'Font Size',
- description: 'Adjust the font size.',
- default: '13',
- showInSettings: true,
- updateStylesheet: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'autoshow',
- default: true,
- title: 'Autoshow',
- description: 'Automatically show the player when the thread contains sounds.',
- showInSettings: false,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'pauseOnHide',
- default: false,
- title: 'Pause on hide',
- description: 'Pause the player when it\'s hidden.',
- showInSettings: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'showSoundTagOnly',
- default: false,
- title: '<span style="margin: 0.2em 0;">Only Show<br>Sound Posts</span>',
- description: 'When enabled, only posts with [sound=URL] tags will be displayed in the playlist.',
- showInSettings: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'borderWidth',
- default: '1px',
- title: 'Border Width',
- showInSettings: true,
- updateStylesheet: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.forceBorderWidth',
- }],
- },
- ]
- },
- {
- title: 'Media Display Settings',
- description: 'Settings for media display dimensions.',
- showInSettings: true,
- settings: [{
- property: 'minMediaHeight',
- title: 'Minimum Height',
- description: 'Maximum width for the Media Display.',
- default: '25px',
- showInSettings: true,
- updateStylesheet: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'maxMediaHeight',
- title: 'Maximum Height',
- description: 'Maximum height for the Media Display.',
- default: '400px',
- showInSettings: true,
- updateStylesheet: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- ]
- },
- {
- title: 'Minimised Display',
- description: 'Optional displays for when the player is minimised.',
- settings: [{
- property: 'pip',
- title: 'Enabled',
- description: 'Display a fixed Minimised Display of the playing sound in the bottom right of the thread.',
- default: true,
- showInSettings: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'maxPIPWidth',
- title: 'Maximum Width',
- description: 'Maximum width for the Minimised Display.',
- default: '200px',
- updateStylesheet: true,
- showInSettings: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'maxPIPHeight',
- title: 'Maximum Height',
- description: 'Maximum height for the Minimised Display.',
- default: '150px',
- updateStylesheet: true,
- showInSettings: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'offsetBottomPIP',
- title: 'Bottom offset',
- description: 'Changes the bottom offset (position) of the minimized player.',
- default: '10px',
- updateStylesheet: true,
- showInSettings: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'offsetRightPIP',
- title: 'Right offset',
- description: 'Changes the right offset (position) of the minimized player.',
- default: '10px',
- updateStylesheet: true,
- showInSettings: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'zIndexPIP',
- title: 'Z-Index',
- 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.',
- default: '0',
- updateStylesheet: true,
- showInSettings: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'chanXControls',
- title: '4chan X Header Controls',
- description: 'Show playback controls in the 4chan X header. Customise the template below.',
- showInSettings: isChanX,
- options: {
- always: 'Always',
- closed: 'Only with the player closed',
- never: 'Never'
- }
- }
- ]
- },
- {
- title: "Controls",
- displayGroup: "Display",
- showInSettings: true,
- settings: [{
- property: "preventControlWrapping",
- title: "Prevent Wrapping",
- description: "Hide elements from controls to prevent wrapping when the player is too small",
- default: true,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: "controlsHideOrder",
- title: "Hide Order",
- description: 'Order controls are hidden in to prevent wrapping. ' +
- 'Available controls are\n' +
- 'previous, ' +
- 'next, ' +
- 'seek-bar, ' +
- 'time, ' +
- 'duration, ' +
- 'volume-bar ' +
- 'and fullscreen.',
- default: ["fullscreen", "seek-bar", "duration", "time", "volume-bar", "previous", "next"],
- showInSettings: 'textarea',
- attrs: 'style="height:120px;"',
- split: '\n',
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- }]
- },
- {
- title: 'Limit Post Width',
- description: 'Limit the width of posts so they aren\'t hidden under the player.',
- showInSettings: true,
- settings: [{
- property: 'limitPostWidths',
- title: 'Enabled',
- default: false,
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'minPostWidth',
- title: 'Minimum Width',
- default: '30%',
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- }
- ]
- },
- {
- property: 'threadsViewStyle',
- title: 'Threads View',
- description: 'How threads in the threads view are listed.',
- showInSettings: false,
- settings: [{
- title: 'Display',
- default: 'table',
- options: {
- table: 'Table',
- board: 'Board'
- }
- }]
- },
- {
- title: 'Keybinds',
- showInSettings: true,
- description: 'Enable keyboard shortcuts.',
- format: 'hotkeys.stringifyKey',
- parse: 'hotkeys.parseKey',
- class: `${ns}-key-input`,
- property: 'hotkey_bindings',
- settings: [{
- property: 'hotkeys',
- default: 'open',
- handler: 'hotkeys.apply',
- title: 'Enabled',
- format: null,
- parse: null,
- class: null,
- options: {
- always: 'Always',
- open: 'Only with the player open',
- never: 'Never'
- }
- },
- {
- property: 'hotkey_bindings.playPause',
- title: 'Play/Pause',
- keyHandler: 'togglePlay',
- ignoreRepeat: true,
- default: {
- key: ' '
- },
- actions: [{
- title: 'R',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'hotkey_bindings.previous',
- title: 'Previous',
- keyHandler: 'previous',
- ignoreRepeat: true,
- default: {
- key: 'arrowleft'
- },
- actions: [{
- title: 'R',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'hotkey_bindings.next',
- title: 'Next',
- keyHandler: 'next',
- ignoreRepeat: true,
- default: {
- key: 'arrowright'
- },
- actions: [{
- title: 'R',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'hotkey_bindings.volumeUp',
- title: 'Volume Up',
- keyHandler: 'hotkeys.volumeUp',
- default: {
- shiftKey: true,
- key: 'arrowup'
- },
- actions: [{
- title: 'R',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'hotkey_bindings.volumeDown',
- title: 'Volume Down',
- keyHandler: 'hotkeys.volumeDown',
- default: {
- shiftKey: true,
- key: 'arrowdown'
- },
- actions: [{
- title: 'R',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'hotkey_bindings.toggleFullscreen',
- title: 'Toggle Fullscreen',
- keyHandler: 'display.toggleFullScreen',
- default: {
- key: ''
- },
- actions: [{
- title: 'R',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'hotkey_bindings.togglePlayer',
- title: 'Show/Hide',
- keyHandler: 'display.toggle',
- default: {
- key: 'h'
- },
- actions: [{
- title: 'R',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'hotkey_bindings.togglePlaylist',
- title: 'Toggle Playlist',
- keyHandler: 'playlist.toggleView',
- default: {
- key: ''
- },
- actions: [{
- title: 'R',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'hotkey_bindings.scrollToPlaying',
- title: 'Jump To Playing',
- keyHandler: 'playlist.scrollToPlaying',
- default: {
- key: ''
- },
- actions: [{
- title: 'R',
- handler: 'settings.reset'
- }],
- },
- {
- property: 'hotkey_bindings.toggleHoverImages',
- title: 'Toggle Hover Images',
- keyHandler: 'playlist.toggleHoverImages',
- default: {
- key: ''
- },
- actions: [{
- title: 'R',
- handler: 'settings.reset'
- }],
- }
- ]
- },
- {
- property: 'allow',
- title: 'Allowed Hosts',
- description: 'Which domains sources are allowed to be loaded from.',
- default: [
- '4cdn.org',
- '8chan.se',
- '8chan.moe',
- 'catbox.moe',
- 'dmca.gripe',
- 'lewd.se',
- 'pomf.cat',
- 'zz.ht'
- ],
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- showInSettings: true,
- split: '\n'
- },
- {
- property: 'filters',
- default: ['# Image MD5 or sound URL'],
- title: 'Filters',
- description: 'List of URLs or image MD5s to filter, one per line.\nLines starting with a # will be ignored.',
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- showInSettings: true,
- split: '\n'
- },
- {
- property: 'headerTemplate',
- title: 'Header Contents',
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- //default: 'repeat-button shuffle-button hover-images-button playlist-button\nsound-name\nadd-button reload-button threads-button settings-button close-button',
- default: 'repeat-button shuffle-button hover-images-button playlist-button \nsound-name\n add-button reload-button settings-button close-button',
- showInSettings: 'textarea',
- },
- {
- property: 'rowTemplate',
- title: 'Row Contents',
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- default: 'sound-name h:{menu-button}',
- showInSettings: 'textarea'
- },
- {
- property: 'footerTemplate',
- title: 'Footer Contents',
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- default:
- '<div class="fc-sounds-footer-left">\n' +
- ' playing-button:"sound-index /" sound-count ui-files-icon\n' +
- '</div>\n' +
- 'p:{\n' +
- ' <div class="fc-sounds-footer-right">\n' +
- ' \n' +
- ' sound-tag-toggle-button\n' +
- ' post-link\n' +
- ' \n' +
- ' <span class="fc-sounds-footer-text"> Open:</span>\n' +
- ' \n' +
- ' ui-bracketL-icon\n' +
- ' image-link sound-link\n' +
- ' ui-bracketR-icon\n' +
- ' \n' +
- ' <span class="fc-sounds-footer-text">Download:</span>\n' +
- ' \n' +
- ' ui-bracketL-icon\n' +
- ' dl-image-button dl-sound-button\n' +
- ' ui-bracketR-icon\n' +
- ' \n' +
- ' </div>\n' +
- '}',
- description: 'Template for the footer contents',
- showInSettings: 'textarea',
- attrs: 'style="height:120px;"'
- },
- {
- property: 'chanXTemplate',
- title: '4chan X Header Controls',
- default: 'p:{\n\tpost-link:"sound-name"\n\tprev-button\n\tplay-button\n\tnext-button\n\tsound-current-time / sound-duration\n}',
- actions: [{
- title: 'Reset',
- handler: 'settings.reset'
- }],
- /*showInSettings: 'textarea'*/
- showInSettings: false,
- },
- {
- title: 'Colors',
- showInSettings: true,
- property: 'colors',
- updateStylesheet: true,
- actions: [{
- title: 'Match Theme',
- handler: 'settings.forceBoardTheme'
- }],
- // These colors will be overriden with the theme defaults at initialization.
- settings: [{
- property: 'colors.text',
- default: 'rgba(0, 0, 0, 1)',
- title: 'Text'
- },
- {
- property: 'colors.background',
- default: 'rgba(214, 218, 240, 1)',
- title: 'Background'
- },
- {
- property: 'colors.border',
- default: 'rgba(183, 197, 217, 1)',
- title: 'Border'
- },
- {
- property: 'colors.odd_row',
- default: 'rgba(214, 218, 240, 1)',
- title: 'Odd Row',
- },
- {
- property: 'colors.even_row',
- default: 'rgba(183, 197, 217, 1)',
- title: 'Even Row'
- },
- {
- property: 'colors.playing',
- default: 'rgba(152, 191, 247, 1)',
- title: 'Playing Row'
- },
- {
- property: 'colors.dragging',
- default: 'rgba(195, 150, 200, 1)',
- title: 'Dragging Row'
- },
- {
- property: 'colors.text_playing',
- default: 'rgba(0, 0, 0, 1)',
- title: '<span style="font-size: 13.5px; margin: 0.2em 0;">Text color of the<br>playing/dragging row</span>'
- },
- {
- property: 'colors.controls_panel',
- default: 'rgba(63, 63, 68, 1)',
- title: '<span style="font-size: 13.5px; margin: 0.2em 0;">Playback Controls<br>Panel Background</span>',
- },
- {
- property: 'colors.buttons_color',
- default: 'rgba(255, 255, 255, 1)',
- title: 'Buttons'
- },
- {
- property: 'colors.hover_color',
- default: 'rgba(0, 182, 240, 1)',
- title: 'Hover',
- },
- {
- property: 'colors.controls_current_time',
- default: 'rgba(255, 255, 255, 1)',
- title: 'Current Time'
- },
- {
- property: 'colors.controls_duration',
- default: 'rgba(144, 144, 144, 1)',
- title: 'Duration'
- },
- {
- property: 'colors.progress_bar',
- default: 'rgba(140, 140, 140, 1)',
- title: '<span style="margin: 0.2em 0;">Progress Bar<br>Background</span>',
- },
- {
- property: 'colors.progress_bar_loaded',
- default: 'rgba(90, 90, 91, 1)',
- title: '<span style="margin: 0.25em 0 0.2em 0;">Loaded Bar<br>Background</span>',
- }
- ]
- },
- ];
- }),
- /* 2 - Core Player Setup
- • Initializes the main Player object with:
- o Component references (controls, playlist, etc.)
- o Template system
- o Event system
- • Key functions:
- o initialize(): Bootstraps all components
- o compareIds(): For sorting sounds
- o acceptedSound(): Validates URLs against allowlist
- o syncTab(): Handles cross-tab synchronization
- */
- (function(module, exports, __webpack_require__) {
- const components = {
- // Settings must be first.
- settings: __webpack_require__(5),
- controls: __webpack_require__(6),
- display: __webpack_require__(7),
- events: __webpack_require__(8),
- footer: __webpack_require__(9),
- header: __webpack_require__(10),
- hotkeys: __webpack_require__(11),
- minimised: __webpack_require__(12),
- playlist: __webpack_require__(13),
- position: __webpack_require__(14),
- threads: __webpack_require__(15),
- userTemplate: __webpack_require__(17)
- };
- // Create a global ref to the player.
- const Player = window.Player = module.exports = {
- //ns,
- audio: new Audio(),
- sounds: [],
- isHidden: true,
- container: null,
- ui: {},
- // Build the config from the default
- config: {},
- // Helper function to query elements in the player.
- $: (...args) => Player.container && Player.container.querySelector(...args),
- $all: (...args) => Player.container && Player.container.querySelectorAll(...args),
- // Store a ref to the components so they can be iterated.
- components,
- // Get all the templates.
- templates: {
- body: __webpack_require__(19),
- controls: __webpack_require__(20),
- css: __webpack_require__(21),
- footer: __webpack_require__(22),
- header: __webpack_require__(23),
- itemMenu: __webpack_require__(24),
- list: __webpack_require__(25),
- player: __webpack_require__(26),
- settings: __webpack_require__(27),
- threads: __webpack_require__(28),
- threadBoards: __webpack_require__(29),
- threadList: __webpack_require__(30)
- },
- /**
- * Set up the player.
- */
- initialize: async function initialize() {
- if (Player.initialized) {
- return;
- }
- Player.initialized = true;
- try {
- Player.sounds = [];
- // Run the initialisation for each component.
- for (let name in components) {
- components[name].initialize && await components[name].initialize();
- }
- if (!is4chan) {
- // Add a sounds link in the nav for archives
- const nav = document.querySelector('.navbar-inner .nav:nth-child(2)');
- const li = createElement('<li><a href="javascript:;">Sounds</a></li>', nav);
- li.children[0].addEventListener('click', Player.display.toggle);
- } else if (isChanX) {
- // If it's already known that 4chan X is running then setup the button for it.
- Player.display.initChanX();
- } else {
- // Add the [Sounds] link in the top and bottom nav.
- document.querySelectorAll('#settingsWindowLink, #settingsWindowLinkBot').forEach(function(link) {
- const showLink = createElement('<a href="javascript:;">Sounds</a>', null, {
- click: Player.display.toggle
- });
- link.parentNode.insertBefore(showLink, link);
- link.parentNode.insertBefore(document.createTextNode('] ['), link);
- });
- }
- // Render the player, but not neccessarily show it.
- Player.display.render();
- // show the player
- Player.display.show();
- } catch (err) {
- Player.logError('There was an error initialzing the sound player. Please check the console for details.');
- console.error('[8chan sounds player]', err);
- // Can't recover so throw this error.
- throw err;
- }
- },
- /**
- * Compare two ids for sorting.
- */
- compareIds: function(a, b) {
- const [aPID, aSID] = a.split(':');
- const [bPID, bSID] = b.split(':');
- const postDiff = aPID - bPID;
- return postDiff !== 0 ? postDiff : aSID - bSID;
- },
- /**
- * Check whether a sound src and image are allowed and not filtered.
- */
- acceptedSound: function({
- src,
- imageMD5
- }) {
- try {
- const link = new URL(src);
- const host = link.hostname.toLowerCase();
- return !Player.config.filters.find(v => v === imageMD5 || v === host + link.pathname) &&
- Player.config.allow.find(h => host === h || host.endsWith('.' + h));
- } catch (err) {
- return false;
- }
- },
- /**
- * Listen for changes
- */
- syncTab: (property, callback) => GM_addValueChangeListener(property, (_prop, oldValue, newValue, remote) => {
- remote && callback(newValue, oldValue);
- }),
- /**
- * Send an error notification event.
- */
- logError: function(message, type = 'error') {
- console.error(message);
- document.dispatchEvent(new CustomEvent('CreateNotification', {
- bubbles: true,
- detail: {
- type: type,
- content: message,
- lifetime: 5
- }
- }));
- }
- };
- // Add each of the components to the player.
- for (let name in components) {
- Player[name] = components[name];
- (Player[name].atRoot || []).forEach(k => Player[k] = Player[name][k]);
- }
- }),
- /* 3 - Main Entry Point
- • Initialization sequence:
- a. Waits for DOM/4chan X readiness
- b. Sets up mutation observer for dynamic content
- c. Triggers initial page scan
- • Handles both:
- o Native 4chan interface
- o 4chan X extension environment
- */
- (function(module, __webpack_exports__, __webpack_require__) {
- "use strict";
- __webpack_require__.r(__webpack_exports__);
- const _globals__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
- const _player__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
- const _file_parser__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(0);
- async function doInit() {
- setTimeout(async function() {
- await _player__WEBPACK_IMPORTED_MODULE_1__.initialize();
- Player.set('showSoundTagOnly', false);
- // Initialize header and footer buttons
- _player__WEBPACK_IMPORTED_MODULE_1__.display.initHeader();
- _player__WEBPACK_IMPORTED_MODULE_1__.display.initFooter();
- // Parse existing posts
- _file_parser__WEBPACK_IMPORTED_MODULE_2__.parseFiles(document.body, true);
- // Add sounds link to 8chan navigation
- const nav = document.querySelector('.threadBottom .innerUtility');
- if (nav && !document.querySelector('.innerUtility a[href="javascript:;"]')) {
- const li = createElement('<a href="javascript:;">Sounds</a>', nav);
- nav.insertBefore(document.createTextNode(' ['), li);
- nav.insertBefore(li, nav.querySelector('.archiveLinkThread'));
- nav.insertBefore(document.createTextNode('] '), nav.querySelector('.archiveLinkThread'));
- li.addEventListener('click', _player__WEBPACK_IMPORTED_MODULE_1__.display.toggle);
- }
- _file_parser__WEBPACK_IMPORTED_MODULE_2__.parseFiles(document.body, true);
- // Set up mutation observer
- const observer = new MutationObserver(function(mutations) {
- mutations.forEach(function(mutation) {
- if (mutation.type === 'childList') {
- mutation.addedNodes.forEach(function(node) {
- if (node.nodeType === Node.ELEMENT_NODE) {
- _file_parser__WEBPACK_IMPORTED_MODULE_2__.parseFiles(node);
- }
- });
- }
- });
- });
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
- }, 0);
- }
- document.addEventListener('DOMContentLoaded', doInit);
- }),
- /* 4 - Globals & Utilities
- • Defines shared utilities:
- o _set()/_get(): Deep object property access
- o toDuration(): Formats time (00:00)
- o timeAgo(): Relative time formatting
- o createElement(): DOM creation helper
- o noDefault(): Event handler wrapper
- • Sets global constants:
- o ns: Namespace prefix
- o is4chan/isChanX: Environment detection
- o Board: Current board name
- o VERSION
- • Load in glyphs
- */
- (function(module, exports, __webpack_require__) {
- // Update globals for 8chan
- window.ns = 'fc-sounds';
- window.is4chan = false;
- window.isChanX = false;
- window.Board = location.pathname.split('/')[1];
- window.localFileCounter = 0;
- window.isLoading = false;
- const scriptVersion = GM_info.script.version;
- window.VERSION = scriptVersion ? scriptVersion : 'Version not found';
- // Keep rest of original globals.js content
- window._set = function(object, path, value) {
- const props = path.split('.');
- const lastProp = props.pop();
- const setOn = props.reduce((obj, k) => obj[k] || (obj[k] = {}), object);
- setOn && (setOn[lastProp] = value);
- return object;
- };
- window._get = function(object, path, dflt) {
- const props = path.split('.');
- const lastProp = props.pop();
- const parent = props.reduce((obj, k) => obj && obj[k], object);
- return parent && Object.prototype.hasOwnProperty.call(parent, lastProp) ?
- parent[lastProp] :
- dflt;
- };
- window.toDuration = function(number) {
- number = Math.floor(number || 0);
- let [seconds, minutes, hours] = _duration(0, number);
- seconds < 10 && (seconds = '0' + seconds);
- return (hours ? hours + ':' : '') + minutes + ':' + seconds;
- };
- window.timeAgo = function(date) {
- const [seconds, minutes, hours, days, weeks] = _duration(Math.floor(date), Math.floor(Date.now() / 1000));
- /* _eslint-disable indent */
- return weeks > 1 ? weeks + ' weeks ago' :
- days > 0 ? days + (days === 1 ? ' day' : ' days') + ' ago' :
- hours > 0 ? hours + (hours === 1 ? ' hour' : ' hours') + ' ago' :
- minutes > 0 ? minutes + (minutes === 1 ? ' minute' : ' minutes') + ' ago' :
- seconds + (seconds === 1 ? ' second' : ' seconds') + ' ago';
- /* eslint-enable indent */
- };
- function _duration(from, to) {
- const diff = Math.max(0, to - from);
- return [
- diff % 60,
- Math.floor(diff / 60) % 60,
- Math.floor(diff / 60 / 60) % 24,
- Math.floor(diff / 60 / 60 / 24) % 7,
- Math.floor(diff / 60 / 60 / 24 / 7)
- ];
- }
- window.createElement = function(html, parent, events = {}) {
- const container = document.createElement('div');
- container.innerHTML = html;
- const el = container.children[0];
- parent && parent.appendChild(el);
- for (let event in events) {
- el.addEventListener(event, events[event]);
- }
- return el;
- };
- window.createElementBefore = function(html, before, events = {}) {
- const el = createElement(html, null, events);
- before.parentNode.insertBefore(el, before);
- return el;
- };
- window.noDefault = (f, ...args) => e => {
- e.preventDefault();
- const func = typeof f === 'function' ? f : _get(Player, f);
- func(...args);
- };
- window.throttleFc = function(func, limit) {
- let inThrottle;
- return function() {
- const args = arguments;
- const context = this;
- if (!inThrottle) {
- func.apply(context, args);
- inThrottle = true;
- setTimeout(() => inThrottle = false, limit);
- }
- }
- };
- window.debounceFc = function(func, timeout = 300){
- let timer;
- return (...args) => {
- clearTimeout(timer);
- timer = setTimeout(() => { func.apply(this, args); }, timeout);
- };
- };
- }),
- /* 5 - Settings Manager
- • Manages all user configuration:
- o load()/save(): Persistent storage
- o set(): Updates settings with validation
- o applyBoardTheme(): Matches 8chan's colors
- • Handles:
- o Settings UI rendering
- o Change detection
- o Cross-tab synchronization
- */
- (function(module, exports, __webpack_require__) {
- const settingsConfig = __webpack_require__(1);
- module.exports = {
- atRoot: ['set'],
- delegatedEvents: {
- click: {
- [`.${ns}-settings .${ns}-heading-action`]: 'settings.handleAction',
- },
- focusout: {
- [`.${ns}-settings input, .${ns}-settings textarea`]: 'settings.handleChange'
- },
- change: {
- [`.${ns}-settings input[type=checkbox], .${ns}-settings select`]: 'settings.handleChange'
- },
- keydown: {
- [`.${ns}-key-input`]: 'settings.handleKeyChange',
- },
- keyup: {
- [`.${ns}-encoded-input`]: 'settings._handleEncoded',
- [`.${ns}-decoded-input`]: 'settings._handleDecoded'
- }
- },
- initialize: async function() {
- // Apply the default config.
- Player.config = settingsConfig.reduce(function reduceSettings(config, setting) {
- if (setting.settings) {
- setting.settings.forEach(subSetting => {
- let _setting = {
- ...setting,
- ...subSetting
- };
- _set(config, _setting.property, _setting.default);
- });
- return config;
- }
- return _set(config, setting.property, setting.default);
- }, {});
- // Load the user config.
- await Player.settings.load();
- // Apply the default board theme as default.
- Player.settings.applyBoardTheme();
- // Listen for the player closing to apply the pause on hide setting.
- Player.on('hide', function() {
- if (Player.config.pauseOnHide) {
- Player.pause();
- }
- });
- // Listen for changes from other tabs
- Player.syncTab('settings', value => Player.settings.apply(value, {
- bypassSave: true,
- applyDefault: true,
- ignore: ['viewStyle']
- }));
- },
- render: function() {
- if (Player.container) {
- Player.$(`.${ns}-settings`).innerHTML = Player.templates.settings();
- }
- },
- forceBorderWidth: function() {
- Player.settings.applyBorderWidth(true);
- Player.settings.save();
- },
- applyBorderWidth: function(force) {
- const innerPostStyle = window.getComputedStyle(document.querySelector('.divPosts .innerPost'));
- let borderWidth = innerPostStyle.getPropertyValue('border-right-width') || '1px';
- borderWidth = Math.max(0.1, Math.min(2, /*Math.round(*/parseFloat(borderWidth)/*)*/)) + 'px' || '1px';
- Player.set('borderWidth', borderWidth, { bypassSave: true, bypassRender: true });
- // Updated the stylesheet if it exists.
- Player.stylesheet && Player.display.updateStylesheet();
- // Re-render the settings if needed.
- Player.settings.render();
- },
- forceBoardTheme: function() {
- Player.settings.applyBoardTheme(true);
- Player.settings.save();
- },
- applyBoardTheme: function(force) {
- const rootStyles = window.getComputedStyle(document.documentElement);
- //console.log(rootStyles);
- const linkStyle = window.getComputedStyle(document.querySelector('.panelBacklinks a'));
- const innerPostStyle = window.getComputedStyle(document.querySelector('.divPosts .innerPost'));
- const selectedTheme = localStorage.getItem('selectedTheme');
- let textColor = rootStyles.getPropertyValue('--text-color').trim() || 'rgba(0,0,0,1)';
- let linkColor = linkStyle.getPropertyValue('color') || rootStyles.getPropertyValue('--link-color').trim() || 'rgba(152,191,247,1)';
- let backgroundColor = innerPostStyle.getPropertyValue('background-color') || rootStyles.getPropertyValue('--contrast-color').trim() || rootStyles.getPropertyValue('--background-color').trim() || 'rgba(255,255,255,1)';
- let borderColor = innerPostStyle.getPropertyValue('border-bottom-color') || rootStyles.getPropertyValue('--horizon-sep-color').trim() || rootStyles.getPropertyValue('--border-color').trim() || 'rgba(183,197,217,1)';
- let linkHoverColor = rootStyles.getPropertyValue('--link-hover-color').trim() || 'rgba(53,133,244,1)';
- let windowsColor = rootStyles.getPropertyValue('--windows-focused-background').trim() || null;
- 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 });
- linkColor = Player.settings.adjustColor(linkColor, { h: 0, s: 0, v: 0, a:1 });
- backgroundColor = Player.settings.adjustColor(backgroundColor, { h: 0, s: 0, v: 0, a:1 });
- borderColor = Player.settings.adjustColor(borderColor, { h: 0, s: 0, v: 0, a:1 });
- linkHoverColor = Player.settings.adjustColor(linkHoverColor, { h: 0, s: 0, v: 0, a:1 });
- borderColor = (borderColor === backgroundColor) ? Player.settings.mixColors(borderColor, textColor, 0.3) : borderColor;
- 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 });
- const evenRow = Player.settings.mixColors(textColor, oddRow, 0.92);
- const controlsPanel = Player.settings.mixColors(backgroundColor, textColor, 0.1);
- const buttonsColor = (windowsColor !== null) ? Player.settings.adjustColor(windowsColor, { h: 0, s: 0, v: 0, a:1 }) : Player.settings.mixColors(textColor, linkColor, 0.85);
- const hoverColor = linkHoverColor;
- const controlsCurrentTime = textColor;
- const controlsDuration = Player.settings.adjustColor(textColor, { h: 0, s: 0, v: 0, a:0.6 });
- let textPlaying;
- switch (selectedTheme) {
- case "evita":
- textPlaying = textColor;
- break;
- case "vivian":
- textPlaying = 'rgba(208,208,208,1.0)';
- break;
- case "warosu":
- textPlaying = 'rgba(245,245,245,1.0)';
- break;
- default:
- textPlaying = Player.settings.isLightColor(backgroundColor)
- ? (Player.settings.isLightColor(textColor) ? 'rgba(22,22,22,1.0)' : backgroundColor)
- : (Player.settings.isLightColor(textColor) ? 'rgba(208,208,208,1.0)' : backgroundColor);
- }
- const playing = Player.settings.isLightColor(backgroundColor)
- ? Player.settings.adjustColor(buttonsColor, { h: 0, s: 0, v: 0, a:0.62 })
- : Player.settings.adjustColor(buttonsColor, { h: 0, s: 0, v: 0, a:0.42 });
- let dragging = Player.settings.mixColors(backgroundColor, buttonsColor, 0.8);
- dragging = Player.settings.adjustColor(dragging, { h: 0, s: 0, v: 0, a:0.7 });
- let progressBarLoaded = Player.settings.mixColors(backgroundColor, buttonsColor, 0.35);
- progressBarLoaded = Player.settings.mixColors(progressBarLoaded, linkColor, 0.05);;
- const progressBar = Player.settings.adjustColor(progressBarLoaded, { h: 0, s: 0, v: 0, a:0.6 });
- const colorSettingMap = {
- 'colors.text': textColor,
- 'colors.background': backgroundColor,
- 'colors.border': borderColor,
- 'colors.odd_row': oddRow,
- 'colors.even_row': evenRow,
- 'colors.playing': playing,
- 'colors.dragging': dragging,
- 'colors.text_playing': textPlaying,
- 'colors.controls_panel': controlsPanel,
- 'colors.buttons_color': buttonsColor,
- 'colors.hover_color': hoverColor,
- 'colors.controls_current_time': controlsCurrentTime,
- 'colors.controls_duration': controlsDuration,
- 'colors.progress_bar': progressBar,
- 'colors.progress_bar_loaded': progressBarLoaded,
- };
- settingsConfig.find(s => s.property === 'colors').settings.forEach(setting => {
- const updateConfig = force || (setting.default === _get(Player.config, setting.property));
- colorSettingMap[setting.property] && (setting.default = colorSettingMap[setting.property]);
- updateConfig && Player.set(setting.property, setting.default, {
- bypassSave: true,
- bypassRender: true
- });
- });
- // Updated the stylesheet if it exists.
- Player.stylesheet && Player.display.updateStylesheet();
- // Re-render the settings if needed.
- Player.settings.render();
- Player.settings.applyBorderWidth();
- },
- parseColor: function(color) {
- let result;
- // Named HTML colors to hex mapping
- 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"};
- // Convert named color to hex first if it exists
- if (htmlColors[color.toLowerCase()]) {
- color = htmlColors[color.toLowerCase()];
- }
- // Helper function to validate and clamp RGB values
- const clampRGB = (value) => Math.min(255, Math.max(0, parseInt(value, 10)));
- // Helper function to validate and clamp alpha values
- const clampAlpha = (value) => {
- const num = parseFloat(value);
- return Math.min(1, Math.max(0, isNaN(num) ? 1 : num));
- };
- // Hex formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
- if (/^#([0-9A-Fa-f]{3,4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(color)) {
- let hex = color.slice(1);
- // Expand shorthand (e.g., #RGBA → #RRGGBBAA)
- if (hex.length === 3 || hex.length === 4) {
- hex = hex.split('').map(x => x + x).join('');
- }
- // Parse to [r, g, b, a] (alpha defaults to 1 if missing)
- const r = clampRGB(parseInt(hex.slice(0, 2), 16));
- const g = clampRGB(parseInt(hex.slice(2, 4), 16));
- const b = clampRGB(parseInt(hex.slice(4, 6), 16));
- const a = hex.length === 8 ? clampAlpha(parseInt(hex.slice(6, 8), 16) / 255) : 1;
- return [r, g, b, a];
- }
- // RGB: rgb(r, g, b) → [r, g, b, 1]
- else if (/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(color)) {
- const matches = color.match(/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i);
- const r = clampRGB(matches[1]);
- const g = clampRGB(matches[2]);
- const b = clampRGB(matches[3]);
- return [r, g, b, 1];
- }
- // RGBA: rgba(r, g, b, a) → [r, g, b, a]
- 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)) {
- 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);
- const r = clampRGB(matches[1]);
- const g = clampRGB(matches[2]);
- const b = clampRGB(matches[3]);
- const a = clampAlpha(matches[4]);
- return [r, g, b, a];
- }
- // Return null if format is invalid
- return null;
- },
- isLightColor: function(color) {
- const rgba = Player.settings.parseColor(color);
- if (!rgba) return false;
- // Extract RGB components (ignore alpha for luminance calculation)
- const [r, g, b] = rgba;
- // Calculate luminance
- const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
- // Return true if luminance exceeds threshold (102)
- return luminance > 102;
- },
- /**
- * Checks if a color's hue is above (greater than) yellow (60°).
- * @param {string} color - Input color (hex, rgb, rgba, or named color)
- * @returns {boolean|null} - Returns:
- * - `true` if hue > 60° (e.g., greens, blues, purples)
- * - `false` if hue ≤ 60° (e.g., reds, oranges, yellows)
- * - `null` if color is invalid or grayscale (no hue)
- */
- isHueAboveYellow: function(color) {
- const rgba = Player.settings.parseColor(color);
- if (!rgba) return null;
- // Convert RGB to HSV to extract hue
- const [r, g, b] = rgba.map(c => c / 255);
- const [hue] = Player.settings.rgbToHsv(r, g, b);
- // Grayscale check (saturation ≈ 0)
- const saturation = Player.settings.rgbToHsv(r, g, b)[1];
- if (saturation < 0.05) return null;
- // Compare hue to yellow (60° in HSV/HSL)
- return (hue * 360) > 60;
- },
- /*
- * color: rgba(255, 255, 255, 1)
- * h: hue, range (-100 — 100)
- * s: saturation, range (-100 — 100)
- * v: value/brightness, range (-100 — 100)
- * a: alpha, decimal range ( 0 — 1 ) and -1 = keep original alpha
- */
- adjustColor: function(color, { h = 0, s = 0, v = 0, a = -1 } = {}) {
- const rgba = Player.settings.parseColor(color);
- if (!rgba) return color;
- // Normalize RGB to [0, 1] and extract alpha (default: 1)
- let [r, g, b, originalA = 1] = rgba;
- r /= 255; g /= 255; b /= 255;
- // Convert to HSV
- const [hue, sat, val] = Player.settings.rgbToHsv(r, g, b);
- // Adjust Hue (handle negative values by looping)
- let newHue = (hue * 360 + h) % 360; // Apply hue shift
- newHue = newHue < 0 ? newHue + 360 : newHue; // Ensure 0-360 range
- // Adjust Saturation & Value (clamped to 0-1)
- const newSat = Math.min(1, Math.max(0, sat + s / 100));
- const newVal = Math.min(1, Math.max(0, val + v / 100));
- // Handle Alpha (if a=-1, keep original; else clamp to [0, 1])
- const newAlpha = a === -1 ? originalA : Math.min(1, Math.max(0, a));
- // Convert back to RGB
- const [newR, newG, newB] = Player.settings.hsvToRgb(newHue, newSat, newVal);
- // Helper function to validate and clamp alpha values
- const clampAlpha = (value) => {
- const num = parseFloat(value);
- return Math.min(1, Math.max(0, isNaN(num) ? 1 : num));
- };
- // Return as RGBA string
- return `rgba(${Math.round(newR * 255)},${Math.round(newG * 255)},${Math.round(newB * 255)},${clampAlpha(newAlpha.toFixed(2))})`;
- },
- /**
- * Mixes two rgba colors with optional weighting and blending mode
- * @param {string} color1 - First color (rgba)
- * @param {string} color2 - Second color (rgba)
- * @param {object} options - Mixing options:
- * - weight: 0-1 (default 0.5, equal blend)
- * @returns {string} Mixed color in rgba() format
- */
- mixColors: function(color1, color2, weight = 0.5) {
- // Parse the input RGBA strings
- const parseRgba = (rgba) => {
- const match = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([0-9.]+)?\)/);
- if (!match) throw new Error("Invalid RGBA format");
- return {
- r: parseInt(match[1]),
- g: parseInt(match[2]),
- b: parseInt(match[3]),
- a: match[4] !== undefined ? parseFloat(match[4]) : 1,
- };
- };
- const c1 = parseRgba(color1);
- const c2 = parseRgba(color2);
- // Linear interpolation function
- const lerp = (a, b, t) => a + (b - a) * t;
- // Mix the colors
- const a = lerp(c1.a, c2.a, weight);
- const r = Math.round(lerp(c1.r * c1.a, c2.r * c2.a, weight) / a);
- const g = Math.round(lerp(c1.g * c1.a, c2.g * c2.a, weight) / a);
- const b = Math.round(lerp(c1.b * c1.a, c2.b * c2.a, weight) / a);
- // Helper function to validate and clamp alpha values
- const clampAlpha = (value) => {
- const num = parseFloat(value);
- return Math.min(1, Math.max(0, isNaN(num) ? 1 : num));
- };
- return `rgba(${r},${g},${b},${clampAlpha(a.toFixed(2))})`;
- },
- rgbToHsv: function(r, g, b) {
- const max = Math.max(r, g, b);
- const min = Math.min(r, g, b);
- let hVal, sVal, vVal = max;
- const d = max - min;
- sVal = max === 0 ? 0 : d / max;
- if (d === 0) {
- hVal = 0;
- } else {
- switch (max) {
- case r: hVal = (g - b) / d + (g < b ? 6 : 0); break;
- case g: hVal = (b - r) / d + 2; break;
- case b: hVal = (r - g) / d + 4; break;
- }
- hVal /= 6;
- }
- return [hVal, sVal, vVal];
- },
- hsvToRgb: function(h, s, v) {
- const c = v * s;
- const x = c * (1 - Math.abs((h / 60) % 2 - 1));
- const m = v - c;
- let r1, g1, b1;
- if (h < 60) [r1, g1, b1] = [c, x, 0];
- else if (h < 120) [r1, g1, b1] = [x, c, 0];
- else if (h < 180) [r1, g1, b1] = [0, c, x];
- else if (h < 240) [r1, g1, b1] = [0, x, c];
- else if (h < 300) [r1, g1, b1] = [x, 0, c];
- else [r1, g1, b1] = [c, 0, x];
- return [r1 + m, g1 + m, b1 + m];
- },
- /**
- * Update a setting.
- */
- set: function(property, value, {
- bypassSave,
- bypassRender,
- silent
- } = {}) {
- const previousValue = _get(Player.config, property);
- if (previousValue === value) {
- return;
- }
- _set(Player.config, property, value);
- !silent && Player.trigger('config', property, value, previousValue);
- !silent && Player.trigger('config:' + property, value, previousValue);
- !bypassSave && Player.settings.save();
- !bypassRender && Player.settings.findDefault(property).showInSettings && Player.settings.render();
- },
- /**
- * Reset a setting to the default value
- */
- reset: function(property) {
- let settingConfig = Player.settings.findDefault(property);
- Player.set(property, settingConfig.default);
- Player.display.updateStylesheet();
- //Player.settings.render();
- },
- /**
- * Persist the player settings.
- */
- save: function() {
- try {
- // Filter settings that have been modified from the default.
- const settings = settingsConfig.reduce(function _handleSetting(settings, setting) {
- if (setting.settings) {
- setting.settings.forEach(subSetting => _handleSetting(settings, {
- property: setting.property,
- default: setting.default,
- ...subSetting
- }));
- } else {
- const userVal = _get(Player.config, setting.property);
- if (userVal !== undefined && userVal !== setting.default) {
- _set(settings, setting.property, userVal);
- }
- }
- return settings;
- }, {});
- // Show the playlist or image view on load, whichever was last shown.
- settings.viewStyle = Player.playlist._lastView;
- // Store the player version with the settings.
- settings.VERSION = window.VERSION;
- // Save the settings.
- return GM.setValue('settings', JSON.stringify(settings));
- } catch (err) {
- Player.logError('There was an error saving the sound player settings. Please check the console for details.');
- console.error('[8chan sounds player]', err);
- }
- },
- /**
- * Restore the saved player settings.
- */
- load: async function() {
- try {
- let settings = await GM.getValue('settings') || await GM.getValue(ns + '.settings');
- if (settings) {
- Player.settings.apply(settings, {
- bypassSave: true,
- silent: true
- });
- }
- } catch (err) {
- Player.logError('There was an error loading the sound player settings. Please check the console for details.');
- console.error('[8chan sounds player]', err);
- }
- },
- apply: function(settings, opts = {}) {
- if (typeof settings === 'string') {
- settings = JSON.parse(settings);
- }
- settingsConfig.forEach(function _handleSetting(setting) {
- if (setting.settings) {
- return setting.settings.forEach(subSetting => _handleSetting({
- property: setting.property,
- default: setting.default,
- ...subSetting
- }));
- }
- if (opts.ignore && opts.ignore.includes(opts.property)) {
- return;
- }
- const value = _get(settings, setting.property, opts.applyDefault ? setting.default : undefined);
- if (value !== undefined) {
- Player.set(setting.property, value, opts);
- }
- });
- },
- /**
- * Find a setting in the default configuration.
- */
- findDefault: function(property) {
- let settingConfig;
- settingsConfig.find(function(setting) {
- if (setting.property === property) {
- return settingConfig = setting;
- }
- if (setting.settings) {
- let subSetting = setting.settings.find(_setting => _setting.property === property);
- return subSetting && (settingConfig = {
- ...setting,
- settings: null,
- ...subSetting
- });
- }
- return false;
- });
- return settingConfig || {
- property
- };
- },
- /**
- * Toggle whether the player or settings are displayed.
- */
- toggle: function(e) {
- e && e.preventDefault();
- // Blur anything focused so the change is applied.
- let focused = Player.$(`.${ns}-settings :focus`);
- focused && focused.blur();
- if (Player.config.viewStyle === 'settings') {
- Player.playlist.restore();
- } else {
- Player.display.setViewStyle('settings');
- }
- },
- /**
- * Handle the user making a change in the settings view.
- */
- handleChange: function(e) {
- try {
- const input = e.eventTarget;
- const property = input.getAttribute('data-property');
- if (!property) {
- return;
- }
- let settingConfig = Player.settings.findDefault(property);
- // Get the new value of the setting.
- const currentValue = _get(Player.config, property);
- let newValue = input[input.getAttribute('type') === 'checkbox' ? 'checked' : 'value'];
- if (settingConfig.parse) {
- newValue = _get(Player, settingConfig.parse)(newValue);
- }
- if (settingConfig && settingConfig.split) {
- newValue = newValue.split(decodeURIComponent(settingConfig.split));
- }
- // Not the most stringent check but enough to avoid some spamming.
- if (currentValue !== newValue) {
- // Update the setting.
- Player.set(property, newValue, {
- bypassRender: true
- });
- // Update the stylesheet reflect any changes.
- if (settingConfig.updateStylesheet) {
- Player.display.updateStylesheet();
- }
- }
- // Run any handler required by the value changing
- settingConfig && settingConfig.handler && _get(Player, settingConfig.handler, () => null)(newValue);
- } catch (err) {
- Player.logError('There was an error updating the setting. Please check the console for details.');
- console.error('[8chan sounds player]', err);
- }
- },
- /**
- * Converts a key event in an input to a string representation set as the input value.
- */
- handleKeyChange: function(e) {
- e.preventDefault();
- if (e.key === 'Shift' || e.key === 'Control' || e.key === 'Meta') {
- return;
- }
- e.eventTarget.value = Player.hotkeys.stringifyKey(e);
- },
- /**
- * Handle an action link next to a heading being clicked.
- */
- handleAction: function(e) {
- e.preventDefault();
- const property = e.eventTarget.getAttribute('data-property');
- const handlerName = e.eventTarget.getAttribute('data-handler');
- const handler = _get(Player, handlerName);
- handler && handler(property);
- },
- /**
- * Encode the decoded input.
- */
- _handleDecoded: function(e) {
- Player.$(`.${ns}-encoded-input`).value = encodeURIComponent(e.eventTarget.value);
- },
- /**
- * Decode the encoded input.
- */
- _handleEncoded: function(e) {
- Player.$(`.${ns}-decoded-input`).value = decodeURIComponent(e.eventTarget.value);
- }
- };
- }),
- /* 6 - Playback Controls
- • Core audio functions:
- o play()/pause()/togglePlay()
- o next()/previous(): Track navigation
- o _movePlaying(): Handles repeat modes
- • UI controls:
- o Seek bar handling
- o Volume control
- o Progress updates
- • Video sync for webm files
- */
- (function(module, exports) {
- const videoFileExtRE = /\.(webm|mp4|m4v|ogv|avi|mpeg|mpg|mpe|m1v|m2v|mov|wmv)$/i;
- const audioFileExtRE = /\.(mp3|m4a|m4b|flac|ogg|oga|opus|mp2|mpega|wav|aac)$/i;
- const videoMimeRE = /^video\/.+$/;
- const audioMimeRE = /^audio\/.+$/;
- const progressBarStyleSheets = {};
- let syncInterval;
- let Master = undefined;
- let Slave = undefined;
- let responseStatus;
- module.exports = {
- atRoot: ['togglePlay', 'play', 'pause', 'next', 'previous'],
- delegatedEvents: {
- click: {
- [`.${ns}-previous-button`]: () => Player.previous(),
- [`.${ns}-play-button`]: 'togglePlay',
- [`.${ns}-next-button`]: () => Player.next(),
- [`.${ns}-seek-bar`]: 'controls.handleSeek',
- [`.${ns}-volume-bar`]: 'controls.handleVolume',
- [`.${ns}-fullscreen-button`]: 'display.toggleFullScreen',
- [`.${ns}-media:not(.${ns}-pip) .${ns}-image-link`]: 'togglePlay',
- },
- mousedown: {
- [`.${ns}-seek-bar`]: () => Player._seekBarDown = true,
- [`.${ns}-volume-bar`]: () => Player._volumeBarDown = true
- },
- mousemove: {
- [`.${ns}-seek-bar`]: e => Player._seekBarDown && Player.controls.handleSeek(e),
- [`.${ns}-volume-bar`]: e => Player._volumeBarDown && Player.controls.handleVolume(e)
- }
- },
- undelegatedEvents: {
- ended: {
- [`.${ns}-video`]: 'controls.handleSoundEnded'
- },
- mouseleave: {
- [`.${ns}-seek-bar`]: e => Player._seekBarDown && Player.controls.handleSeek(e),
- [`.${ns}-volume-bar`]: e => Player._volumeBarDown && Player.controls.handleVolume(e)
- },
- mouseup: {
- body: () => {
- Player._seekBarDown = false;
- Player._volumeBarDown = false;
- }
- }
- },
- soundEvents: {
- ended: 'controls.handleSoundEnded',
- pause: 'controls.handlePlaybackState',
- play: 'controls.handlePlaybackState',
- seeked: 'controls.handlePlaybackState',
- playing: 'controls.handlePlaybackState',
- waiting: 'controls.handlePlaybackState',
- timeupdate: 'controls.updateDuration',
- loadedmetadata: 'controls.updateDuration',
- durationchange: 'controls.updateDuration',
- volumechange: 'controls.updateVolume',
- loadstart: 'controls.pollForLoading',
- error: 'controls.handleSoundError',
- },
- audioEvents: {
- ended: 'controls.handleSoundEnded',
- pause: 'controls.handlePlaybackState',
- play: 'controls.handlePlaybackState',
- //seeked: 'controls.handlePlaybackState',
- //playing: 'controls.handlePlaybackState',
- //waiting: 'controls.handlePlaybackState',
- timeupdate: 'controls.updateDuration',
- //loadedmetadata: 'controls.updateDuration',
- durationchange: 'controls.updateDuration',
- //volumechange: 'controls.updateVolume',
- //loadstart: 'controls.pollForLoading',
- },
- initialize: function() {
- Player.on('show', () => Player._hiddenWhilePolling && Player.controls.pollForLoading());
- Player.on('hide', () => {
- Player._hiddenWhilePolling = !!Player._loadingPoll;
- Player.controls.stopPollingForLoading();
- });
- // Initialize loop state based on current repeat mode
- const updateLoop = () => {
- const video = document.querySelector(`.${ns}-video`);
- // if durations don't equal ±2 seconds difference.
- if (Slave !== undefined && (Math.abs(Master.duration - Slave.duration) > 2)) {
- video.loop = true;
- Player.audio.loop = Player.config.repeat === 'one';
- return;
- }
- video.loop = Player.config.repeat === 'one';
- Player.audio.loop = Player.config.repeat === 'one';
- };
- // Listen for repeat mode changes through Player events
- Player.on('config:repeat', updateLoop);
- document.addEventListener('visibilitychange', () => {
- // video starts to lag when window is in background, this should get it back to normal speed on tab in + should fix sync
- if (!document.hidden && Player.playing && Master !== undefined && !Master.paused) {
- const video = document.querySelector(`.${ns}-video`);
- if (isFinite(Master.duration) && Slave !== undefined && (Math.abs(Master.duration - video.duration) < 2) || isFinite(Master.duration) && Slave === undefined) {
- // Try to resume playback when tab becomes visible
- const currentTime = Master.currentTime;
- Master.currentTime = 0;
- video.currentTime = 0;
- Master.currentTime = currentTime;
- video.currentTime = currentTime;
- }
- Master.play().catch(() => {});
- video.play().catch(() => {});
- Player.controls.handlePlaybackState(); // Resync UI
- }
- });
- Player.on('rendered', () => {
- // Keep track of heavily updated elements.
- Player.ui.currentTimeBar = Player.$(`.${ns}-seek-bar .${ns}-current-bar`);
- Player.ui.loadedBar = Player.$(`.${ns}-seek-bar .${ns}-loaded-bar`);
- Player.on('rendered', () => {
- Player.ui.currentTimeBar = Player.$(`.${ns}-seek-bar .${ns}-current-bar`);
- Player.ui.loadedBar = Player.$(`.${ns}-seek-bar .${ns}-loaded-bar`);
- });
- // video event listeners
- const video = document.querySelector(`.${ns}-video`);
- if (video) {
- Object.entries(Player.controls.soundEvents).forEach(([event, handler]) => {
- // Handle both string paths and direct function references
- const handlerFn = typeof handler === 'function'
- ? handler
- : _get(Player, handler);
- video.addEventListener(event, handlerFn);
- });
- }
- // audio element event listeners
- Object.entries(Player.controls.soundEvents).forEach(([event, handler]) => {
- Player.audio.addEventListener(event, Player.controls[handler]);
- });
- // Update repeat mode when player is rendered
- video.loop = Player.config.repeat === 'one';
- Player.audio.loop = Player.config.repeat === 'one';
- // Add stylesheets to adjust the progress indicator of the seekbar and volume bar.
- document.head.appendChild(progressBarStyleSheets[`.${ns}-seek-bar`] = document.createElement('style'));
- document.head.appendChild(progressBarStyleSheets[`.${ns}-volume-bar`] = document.createElement('style'));
- Player.controls.updateVolume();
- });
- },
- /**
- * Switching being playing and paused.
- */
- togglePlay: function() {
- // Return early if currently loading
- if (window.isLoading) return;
- if (!Player.playing) {
- if (Player.sounds.length) {
- return Player.play(Player.sounds[0]);
- }
- return;
- }
- const video = document.querySelector(`.${ns}-video`);
- if (Master !== undefined && Master.ended) {
- Master.currentTime = 0;
- video.currentTime = 0;
- Master.play();
- video.play().catch(() => {});
- } else if (Master !== undefined && Master.paused) {
- video.currentTime = Master.currentTime;
- Master.play();
- video.play().catch(() => {});
- } else {
- if (Master !== undefined) Master.pause();
- if (video) video.pause();
- }
- Player.controls.handlePlaybackState();
- },
- updatePlayButtonState: function() {
- const buttons = document.querySelectorAll(`.${ns}-play-button`);
- buttons.forEach(button => {
- button.disabled = window.isLoading;
- button.style.opacity = window.isLoading ? '0.5' : '1';
- button.style.cursor = window.isLoading ? 'not-allowed' : 'pointer';
- });
- },
- /**
- * Update the sound name display
- */
- updateHeaderText: function(status) {
- const soundNameContainers = document.querySelectorAll(`.${ns}-header.${ns}-row .${ns}-col.${ns}-truncate-text`);
- setTimeout(function(){
- soundNameContainers.forEach(container => {
- const span = container.querySelector('span');
- if (!span) return;
- switch (status) {
- case "Loading":
- if (span.innerHTML !== 'error') span.innerHTML = 'Loading...';
- break;
- case "Error":
- span.innerHTML = 'error';
- break;
- case "Reset":
- span.innerHTML = span.title;
- break;
- default:
- if (span.innerHTML !== 'error') span.innerHTML = span.title;
- break;
- }
- });
- }, 50);
- },
- // Function to safely get file extension (handles multiple dots in filename)
- getFileExtension: function(filename) {
- // Handle edge cases: no extension, hidden files, or filenames ending with dot
- if (!filename || filename.indexOf('.') === -1 || filename.endsWith('.')) {
- return '';
- }
- return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2).toLowerCase();
- },
- detectMimeType: function(url, arrayBuffer, responseType) {
- if(audioMimeRE.test(responseType)) return responseType;
- if(videoMimeRE.test(responseType)) return responseType;
- const extension = Player.controls.getFileExtension(url);
- const bytes = new Uint8Array(arrayBuffer);
- // Check by file signature (magic numbers)
- // MKV / WebM
- if (bytes.length >= 4 &&
- bytes[0] === 0x1A && bytes[1] === 0x45 && bytes[2] === 0xDF && bytes[3] === 0xA3) {
- // Ideally parse to find DocType (e.g., webm or matroska)
- return /*extension === 'webm' ? */'video/webm'/* : 'video/x-matroska'*/;
- }
- // MP4/M4A/M4V/M4B (MPEG-4 containers)
- if (bytes.length >= 8 &&
- ((bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) || // ftyp
- (bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x00 &&
- (bytes[3] === 0x18 || bytes[3] === 0x20) && bytes[4] === 0x66 &&
- bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70))) {
- // Check for specific MP4 subtypes
- if (bytes.length >= 12) {
- if (bytes[8] === 0x4D && bytes[9] === 0x34 && bytes[10] === 0x41 && bytes[11] === 0x20) {
- return 'audio/mp4'; // M4A
- }
- if (bytes[8] === 0x4D && bytes[9] === 0x34 && bytes[10] === 0x56 && bytes[11] === 0x20) {
- return 'video/mp4'; // M4V
- }
- if (bytes[8] === 0x4D && bytes[9] === 0x34 && bytes[10] === 0x42 && bytes[11] === 0x20) {
- return 'audio/mp4'; // M4B (audiobook format, same as M4A)
- }
- if (bytes[8] === 0x71 && bytes[9] === 0x74 && bytes[10] === 0x20 && bytes[11] === 0x20) {
- return 'video/quicktime'; // MOV (QuickTime)
- }
- }
- return 'video/mp4'; // default MP4
- }
- // FLAC
- if (bytes.length >= 4 &&
- bytes[0] === 0x66 &&
- bytes[1] === 0x4C &&
- bytes[2] === 0x61 &&
- bytes[3] === 0x43) {
- return 'audio/flac';
- }
- // OGG (including OGV, OGA, OPUS)
- if (bytes.length >= 4 &&
- bytes[0] === 0x4F &&
- bytes[1] === 0x67 &&
- bytes[2] === 0x67 &&
- bytes[3] === 0x53) {
- // Could be audio or video OGG
- return extension === 'ogv' ? 'video/ogg' : 'audio/ogg';
- }
- // AVI
- if (bytes.length >= 12 &&
- bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 && // RIFF
- bytes[8] === 0x41 && bytes[9] === 0x56 && bytes[10] === 0x49 && bytes[11] === 0x20) { // AVI
- return 'video/x-msvideo';
- }
- // WAV
- if (bytes.length >= 12 &&
- bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 && // RIFF
- bytes[8] === 0x57 && bytes[9] === 0x41 && bytes[10] === 0x56 && bytes[11] === 0x45) { // WAVE
- return 'audio/wav';
- }
- // MOV (QuickTime)
- if (bytes.length >= 8 &&
- ((bytes[4] === 0x6D && bytes[5] === 0x6F && bytes[6] === 0x6F && bytes[7] === 0x76) || // moov
- (bytes[4] === 0x66 && bytes[5] === 0x72 && bytes[6] === 0x65 && bytes[7] === 0x65))) { // free
- return 'video/quicktime';
- }
- // WMV/ASF
- if (bytes.length >= 16 &&
- bytes[0] === 0x30 && bytes[1] === 0x26 && bytes[2] === 0xB2 && bytes[3] === 0x75 &&
- bytes[4] === 0x8E && bytes[5] === 0x66 && bytes[6] === 0xCF && bytes[7] === 0x11 &&
- bytes[8] === 0xA6 && bytes[9] === 0xD9 && bytes[10] === 0x00 && bytes[11] === 0xAA &&
- bytes[12] === 0x00 && bytes[13] === 0x62 && bytes[14] === 0xCE && bytes[15] === 0x6C) {
- return extension === 'wmv' ? 'video/x-ms-wmv' : 'video/x-ms-asf';
- }
- // MKV (Matroska)
- if (bytes.length >= 4 &&
- bytes[0] === 0x1A && bytes[1] === 0x45 && bytes[2] === 0xDF && bytes[3] === 0xA3) {
- return 'video/x-matroska';
- }
- // MPEG (MP3, MP2, MPEG video)
- if (bytes.length >= 3) {
- // MP3 with ID3 tag
- if (bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33) {
- return 'audio/mpeg';
- }
- // MPEG audio (MP3, MP2) - frame sync
- if ((bytes[0] === 0xFF) && ((bytes[1] & 0xE0) === 0xE0)) {
- // Check layer bits (bits 1-2 of byte 1)
- const layer = (bytes[1] & 0x06) >> 1;
- // Layer 3 (MP3) or Layer 2 (MP2)
- return layer === 3 ? 'audio/mpeg' : 'audio/mpeg'; // MP2 also uses audio/mpeg
- }
- // MPEG video
- if (bytes.length >= 4 &&
- bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 &&
- (bytes[3] >= 0xB0 && bytes[3] <= 0xBF)) {
- return 'video/mpeg';
- }
- }
- // 3GP/3G2 (mobile video formats)
- if (bytes.length >= 12 &&
- bytes[4] === 0x66 && bytes[5] === 0x74 &&
- bytes[6] === 0x79 && bytes[7] === 0x70) { // 'ftyp'
- const brand = String.fromCharCode(bytes[8], bytes[9], bytes[10], bytes[11]);
- // Known 3GP/3G2 brands
- const known3GPBrands = ['3gp4', '3gp5', '3g2a', '3g2b', '3gr6', '3gs7', '3ge6', '3gg6'];
- if (known3GPBrands.includes(brand)) {
- return 'video/3gpp';
- }
- }
- // AAC (Advanced Audio Coding)
- if (bytes.length >= 2 &&
- (bytes[0] === 0xFF && (bytes[1] & 0xF6) === 0xF0)) {
- return 'audio/aac';
- }
- // Fallback to extension-based detection
- switch(extension) {
- case 'webm': return 'video/webm';
- case 'mp4': return 'video/mp4';
- case 'm4a': case 'm4b': return 'audio/mp4';
- case 'm4v': return 'video/mp4';
- case 'flac': return 'audio/flac';
- case 'ogg': case 'oga': return 'audio/ogg';
- case 'ogv': return 'video/ogg';
- case 'opus': return 'audio/ogg';
- case 'avi': return 'video/x-msvideo';
- case 'asx': return 'video/x-ms-asf'; // Advanced Stream Redirector
- case 'mpeg': case 'mpg': case 'mpe': case 'm1v': case 'm2v': return 'video/mpeg';
- case 'mp3': case 'mpega': case 'mp2': return 'audio/mpeg';
- case 'm3u': return 'application/x-mpegurl'; // Playlist file
- default: return 'audio/mpeg'; // default fallback
- }
- },
- BlobXmlHttpRequest: function (src) {
- return new Promise((resolve, reject) => {
- GM.xmlHttpRequest({
- method: 'GET',
- url: src,
- responseType: 'blob',
- onload: function(response) {
- if (response.status >= 400) {
- console.log(new Error(`Failed to fetch media; response.status: ${response.status}, response.responseText: ${response.responseText}`));
- }
- responseStatus = response.status;
- resolve(response.response);
- },
- onerror: reject,
- ontimeout: reject,
- timeout: 60000
- });
- });
- },
- BlobReader: function(blob) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => {
- // Extract only the base64 data after the comma
- const dataUrl = reader.result;
- const base64Data = dataUrl.split(',')[1]; // Split at comma and take the second part
- resolve(base64Data);
- };
- reader.onerror = reject;
- reader.readAsDataURL(blob);
- });
- },
- /**
- * Wait for audio to be ready to play
- */
- waitForAudioReady: function() {
- return new Promise((resolve, reject) => {
- if (!Player.audio) {
- return reject(new Error('Player.audio element not found'));
- }
- // Check if already ready
- if (Player.audio.readyState >= 4) {
- return resolve();
- }
- const onReady = () => {
- cleanup();
- resolve();
- };
- const onError = (err) => {
- cleanup();
- reject(err);
- };
- const cleanup = () => {
- Player.audio.removeEventListener('loadeddata', onReady);
- Player.audio.removeEventListener('error', onError);
- };
- Player.audio.addEventListener('loadeddata', onReady);
- Player.audio.addEventListener('error', onError);
- });
- },
- /**
- * Wait for video to be ready to play
- */
- waitForVideoReady: function() {
- return new Promise((resolve, reject) => {
- const video = document.querySelector(`.${ns}-video`);
- if (!video) {
- return reject(new Error('Video element not found'));
- }
- // Check if already ready
- if (video.readyState >= 4) {
- return resolve();
- }
- const onReady = () => {
- cleanup();
- resolve();
- };
- const onError = (err) => {
- cleanup();
- reject(err);
- };
- const cleanup = () => {
- video.removeEventListener('loadeddata', onReady);
- video.removeEventListener('error', onError);
- };
- video.addEventListener('loadeddata', onReady);
- video.addEventListener('error', onError);
- });
- },
- /**
- * Start playback.
- */
- play: async function(sound) {
- const video = document.querySelector(`.${ns}-video`);
- // if play(sound) and previous play(sound) equal just reset currentTime
- if (Player.playing !== undefined && Master !== undefined && sound.id === Player.playing.id) {
- Master.currentTime = 0;
- video.currentTime = 0;
- Master.play().catch(() => {});
- video.play().catch(() => {});
- Player.controls.handlePlaybackState(); // Resync UI
- return;
- }
- Player.controls.updateHeaderText("Reset");
- window.isLoading = true;
- if (!sound && !Player.playing && Player.sounds.length) {
- sound = Player.sounds[0];
- }
- if (!sound) {
- window.isLoading = false;
- return;
- }
- //console.log(sound);
- Master = undefined;
- Slave = undefined;
- // Clear previous playback
- if (Player.playing) Player.playing.playing = false;
- // Reset media elements completely
- video.pause();
- video.removeAttribute('src');
- video.load();
- video.currentTime = 0;
- Player.audio.pause();
- Player.audio.removeAttribute('src');
- Player.audio.load();
- Player.audio.currentTime = 0;
- Player.controls.updatePlayButtonState();
- try {
- sound.playing = true;
- Player.playing = sound;
- await Player.trigger('playsound', sound);
- // Case 1: hasSoundTag and the sound tag is audio (.mp3, .ogg, .m4a, ...)
- if (sound.hasSoundTag && !sound.isVideo) {
- await Player.controls.updateHeaderText("Loading");
- // First try with GM.xmlHttpRequest
- const response = await Player.controls.BlobXmlHttpRequest(sound.src);
- //console.log(response); console.log('response.type '+response.type); console.log('responseStatus '+responseStatus);
- if (responseStatus < 400) {
- const rawBase64 = await Player.controls.BlobReader(response);
- const mimeType = await Player.controls.detectMimeType(sound.src, rawBase64, response.type);
- Player.audio.src = `data:${mimeType};base64,${rawBase64}`;
- Master = Player.audio;
- Slave = video;
- video.muted = true;
- // Wait for Player.audio to be ready
- await Player.controls.waitForAudioReady();
- if (!isFinite(Master.duration)) {
- // Try to estimate from buffered data
- if (Master.buffered && Master.buffered.length > 0) {
- Master.duration = Master.buffered.end(Master.buffered.length - 1);
- }
- }
- } else {
- console.log(new Error('Failed to fetch via GM_xmlhttpRequest, trying fallback:'));
- Player.controls.updateHeaderText("Error");
- Player.audio.pause();
- Player.audio.removeAttribute('src');
- Player.audio.load();
- Master = video;
- video.muted = false;
- Slave = undefined;
- }
- // Handle video/image element carefully for Case 1
- const imageIsVideo = videoFileExtRE.test(sound.filename); // Check if sound.image is actually a supported video format
- if (imageIsVideo) {
- video.src = sound.image; // Use .image for video if it's a supported format
- // Wait for video to be ready
- await Player.controls.waitForVideoReady();
- await video.play().catch(e => {
- console.log('Video playback failed, falling back to empty source:', e);
- video.pause();
- video.removeAttribute('src');
- video.load();
- Slave = undefined;
- });
- } else {
- video.pause();
- video.removeAttribute('src');
- video.load();
- Slave = undefined;
- }
- // Start playback and Initial sync
- if (responseStatus < 400) {
- await Player.audio.play();
- Player.controls.syncPlayback();
- // Start sync interval
- if (syncInterval) clearInterval(syncInterval);
- syncInterval = setInterval(() => Player.controls.syncPlayback(), 1000);
- }
- }
- // Case 2: hasSoundTag and the sound tag is video (.webm, .mp4)
- else if (sound.hasSoundTag && sound.isVideo) {
- await Player.controls.updateHeaderText("Loading");
- // First try with GM.xmlHttpRequest
- const response = await Player.controls.BlobXmlHttpRequest(sound.src);
- //console.log(response); console.log('response.type '+response.type); console.log('responseStatus '+responseStatus);
- if (responseStatus < 400) {
- const rawBase64 = await Player.controls.BlobReader(response);
- const mimeType = await Player.controls.detectMimeType(sound.src, rawBase64, response.type);
- video.src = `data:${mimeType};base64,${rawBase64}`;
- video.muted = false;
- Master = video;
- // Wait for video to be ready
- await Player.controls.waitForVideoReady();
- if (!isFinite(Master.duration)) {
- // Try to estimate from buffered data
- if (Master.buffered && Master.buffered.length > 0) {
- Master.duration = Master.buffered.end(Master.buffered.length - 1);
- }
- }
- // Start playback
- await video.play();
- } else {
- console.log(new Error('Failed to fetch via GM_xmlhttpRequest, trying fallback:'));
- Player.controls.updateHeaderText("Error");
- // Fallback to direct video playback
- video.src = sound.src;
- video.muted = false;
- Master = video;
- // Wait for video to be ready
- await Player.controls.waitForVideoReady();
- if (!isFinite(Master.duration)) {
- // Try to estimate from buffered data
- if (Master.buffered && Master.buffered.length > 0) {
- Master.duration = Master.buffered.end(Master.buffered.length - 1);
- }
- }
- // Start playback
- await video.play();
- }
- }
- // Case 3: doesn't have hasSoundTag and is video
- else if (!sound.hasSoundTag && sound.isVideo) {
- await Player.controls.updateHeaderText("Loading");
- video.src = sound.src;
- video.muted = false;
- Master = video;
- // Wait for video to be ready
- await Player.controls.waitForVideoReady();
- if (!isFinite(Master.duration)) {
- // Try to estimate from buffered data
- if (Master.buffered && Master.buffered.length > 0) {
- Master.duration = Master.buffered.end(Master.buffered.length - 1);
- }
- }
- // Start playback
- await video.play();
- }
- // Case 4: just audio
- else if (!sound.hasSoundTag && !sound.isVideo) {
- await Player.controls.updateHeaderText("Loading");
- const image = Player.$(`.${ns}-image`);
- Player.audio.src = sound.src;
- image.src = sound.thumb;
- Master = Player.audio;
- // Wait for Player.audio to be ready
- await Player.controls.waitForAudioReady();
- if (!isFinite(Master.duration)) {
- // Try to estimate from buffered data
- if (Master.buffered && Master.buffered.length > 0) {
- Master.duration = Master.buffered.end(Master.buffered.length - 1);
- }
- }
- // Start playback
- await Player.audio.play();
- }
- //console.log('Master: '+Master);
- //console.log('Slave: '+Slave);
- // handlePlaybackState
- Player.controls.handlePlaybackState();
- } catch (err) {
- console.error('Playback error:', err);
- Player.logError('Could not play sound');
- Player.controls.updateHeaderText("Error");
- // Full cleanup
- Player.audio.pause();
- Player.audio.removeAttribute('src');
- Player.audio.load();
- const video = document.querySelector(`.${ns}-video`);
- if (video) {
- video.pause();
- video.removeAttribute('src');
- video.load();
- }
- Master = undefined;
- Slave = undefined;
- if (syncInterval) clearInterval(syncInterval);
- // handlePlaybackState
- Player.controls.handlePlaybackState();
- return Player.next(); // Skip to next track on error
- } finally {
- window.isLoading = false;
- Player.controls.updateHeaderText();
- Player.controls.updatePlayButtonState();
- }
- },
- /**
- * Pause playback.
- */
- pause: function() {
- const video = document.querySelector(`.${ns}-video`);
- if (Master !== undefined) Master.pause();
- if (video) video.pause();
- Player.controls.handlePlaybackState();
- },
- /**
- * Play the next sound.
- */
- next: function(force = true) {
- Player.controls._movePlaying(1, force);
- },
- /**
- * Play the previous sound.
- */
- previous: function(force = true) {
- Player.controls._movePlaying(-1, force);
- },
- _movePlaying: function(direction, force) {
- if (!Player.audio) return;
- if (Master === undefined) return;
- if (!Master.ended && !force) return;
- try {
- // If there's no sound fall out.
- if (!Player.sounds.length) return;
- // If there's no sound currently playing or it's not in the list then just play the first sound.
- const currentIndex = Player.sounds.indexOf(Player.playing);
- if (currentIndex === -1) return Player.play(Player.sounds[0]);
- // Calculate next index based on repeat mode
- let nextIndex;
- if (!force && Player.config.repeat === 'one') return; //let loop handle it
- if (!force && Player.config.repeat === 'none') {
- const video = document.querySelector(`.${ns}-video`);
- Player.pause();
- if (video) video.pause();
- return;
- }
- nextIndex = currentIndex + direction;
- // Handle if (Player.config.repeat === 'all') / Wrap around for 'all' mode
- if (nextIndex >= Player.sounds.length) nextIndex = 0;
- if (nextIndex < 0) nextIndex = Player.sounds.length - 1;
- const nextSound = Player.sounds[nextIndex];
- nextSound && Player.play(nextSound);
- Player.set('showSoundTagOnly', false);
- Player.playlist.applySoundTagFilter();
- } catch (err) {
- Player.logError(`There was an error selecting the ${direction > 0 ? 'next' : 'previous'} track. Please check the console for details.`);
- console.error('[8chan sounds player]', err);
- }
- },
- getCurrentPlaybackPosition: function() {
- if (Master === undefined) return;
- const video = document.querySelector(`.${ns}-video`);
- return Master ? Master.currentTime : 0;
- },
- syncPlayback: async function() {
- if (!Player.playing) return;
- if (Master === undefined || Slave === undefined) return;
- const video = document.querySelector(`.${ns}-video`);
- if (!isFinite(Master.duration)) {
- if (syncInterval) clearInterval(syncInterval);
- return;
- }
- // If nothing is playing or Master isn't available, bail out
- if (!Master || Master.paused) return;
- // if durations don't equal ±2 seconds difference.
- if (Slave && (Math.abs(Master.duration - Slave.duration) > 2)) {
- if (syncInterval) clearInterval(syncInterval);
- video.loop = true;
- Player.audio.loop = Player.config.repeat === 'one';
- return;
- }
- // Sync Slave to Master if it exists and isn't already in sync
- if (Slave && (Math.abs(Slave.currentTime - Master.currentTime) > 0.8)) {
- Slave.currentTime = Master.currentTime;
- }
- },
- handlePlaybackState: function() {
- const video = document.querySelector(`.${ns}-video`);
- const isPlaying = !Player.audio.paused || (video && !video.paused);
- // Update all play buttons
- document.querySelectorAll(`.${ns}-play-button .${ns}-play-button-display`).forEach(el => {
- el.classList.toggle(`${ns}-play`, !isPlaying);
- });
- // Update container state if needed
- if (Player.container) {
- Player.container.classList.toggle(`${ns}-playing`, isPlaying);
- Player.container.classList.toggle(`${ns}-paused`, !isPlaying);
- }
- Player.controls.updateDuration();
- },
- handleSoundEnded: function() {
- Player.next(false);
- },
- /**
- * Handle sound errors
- */
- handleSoundError: function() {
- const video = document.querySelector(`.${ns}-video`);
- // Clean up blob URLs on error
- if (Player.audio.src && Player.audio.src.startsWith('blob:')) {
- URL.revokeObjectURL(Player.audio.src);
- Player.audio.pause();
- Player.audio.removeAttribute('src');
- Player.audio.load();
- }
- if ((Master === video) && video?.error) {
- console.error('Video error:', video.error);
- Player.logError('Video playback error.');
- Player.controls.updateHeaderText("Error");
- } else if (Player.audio?.error) {
- console.error('Audio error:', Player.audio.error);
- Player.logError('Audio playback error.');
- Player.controls.updateHeaderText("Error");
- }
- },
- /**
- * Poll for how much has loaded. I know there's the progress event but it unreliable.
- */
- pollForLoading: function() {
- Player._loadingPoll = Player._loadingPoll || setInterval(Player.controls.updateLoaded, 1000);
- },
- /**
- * Stop polling for how much has loaded.
- */
- stopPollingForLoading: function() {
- Player._loadingPoll && clearInterval(Player._loadingPoll);
- Player._loadingPoll = null;
- },
- /**
- * Update the loading bar.
- */
- updateLoaded: function() {
- if (Master === undefined) return;
- const video = document.querySelector(`.${ns}-video`);
- let duration = Master.duration;
- if (!isFinite(duration)) {
- // Try to estimate from buffered data
- if (Master.buffered && Master.buffered.length > 0) {
- duration = Master.buffered.end(Master.buffered.length - 1);
- }
- }
- if (!Master || !Master.buffered || Master.buffered.length === 0) return;
- const length = Master.buffered.length;
- const size = (Master.buffered.end(length - 1) / duration) * 100;
- if (size === 100) {
- Player.controls.stopPollingForLoading();
- }
- if (Player.ui.loadedBar) {
- Player.ui.loadedBar.style.width = size + '%';
- }
- },
- /**
- * Update the seek bar and the duration labels.
- */
- updateDuration: function() {
- if (!Player.container) return;
- if (Master === undefined) return;
- const video = document.querySelector(`.${ns}-video`);
- let duration = Master.duration;
- if (!isFinite(duration)) {
- // Try to estimate from buffered data
- if (Master.buffered && Master.buffered.length > 0) {
- duration = Master.buffered.end(Master.buffered.length - 1);
- }
- }
- const currentTime = Player.controls.getCurrentPlaybackPosition();
- document.querySelectorAll(`.${ns}-current-time`).forEach(el => el.innerHTML = toDuration(currentTime));
- document.querySelectorAll(`.${ns}-duration`).forEach(el => el.innerHTML = '/'+toDuration(duration));
- Player.controls.updateProgressBarPosition(`.${ns}-seek-bar`, Player.ui.currentTimeBar, currentTime, duration);
- },
- /**
- * Update the volume bar.
- */
- updateVolume: function() {
- Player.controls.updateProgressBarPosition(`.${ns}-volume-bar`, Player.$(`.${ns}-volume-bar .${ns}-current-bar`), Player.audio.volume, 1);
- },
- /**
- * Update a progress bar width. Adjust the margin of the circle so it's contained within the bar at both ends.
- */
- updateProgressBarPosition: function(id, bar, current, total) {
- current || (current = 0);
- total || (total = 0);
- const ratio = !total ? 0 : Math.max(0, Math.min(((current || 0) / total), 1));
- bar.style.width = (ratio * 100) + '%';
- if (progressBarStyleSheets[id]) {
- progressBarStyleSheets[id].innerHTML = `${id} .${ns}-current-bar:after {
- margin-right: ${-0.8 * (1 - ratio)}rem;
- }`;
- }
- },
- /**
- * Handle the user interacting with the seek bar.
- */
- handleSeek: function(e) {
- e.preventDefault();
- if (!Player.playing) return;
- if (Player.playing.playing == false) return;
- if (Master === undefined) return;
- const video = document.querySelector(`.${ns}-video`);
- let duration = Master.duration;
- if (!isFinite(duration)) {
- // Try to estimate from buffered data
- if (Master.buffered && Master.buffered.length > 0) {
- duration = Master.buffered.end(Master.buffered.length - 1);
- }
- }
- if (!Master || !isFinite(duration)) return;
- const ratio = e.offsetX / parseInt(document.defaultView.getComputedStyle(e.eventTarget || e.target).width, 10);
- const seekTime = duration * ratio;
- // Update media elements
- Master.currentTime = seekTime;
- if (Player.playing?.hasSoundTag) {
- if (video) video.currentTime = seekTime;
- }
- if (!Master.paused) {
- Master.play();
- video.play().catch(() => {});
- }
- Player.controls.handlePlaybackState(); // Resync UI
- },
- /**
- * Handle the user interacting with the volume bar.
- */
- handleVolume: function(e) {
- e.preventDefault();
- if (!Player.container) return;
- const ratio = e.offsetX / parseInt(document.defaultView.getComputedStyle(e.eventTarget || e.target).width, 10);
- Player.audio.volume = Math.max(0, Math.min(ratio, 1));
- const video = document.querySelector(`.${ns}-video`);
- if (video) {
- video.volume = Player.audio.volume;
- }
- Player.controls.updateVolume();
- },
- };
- }),
- /* 7 - Display Management
- • Player UI lifecycle:
- o render(): Creates player DOM
- o show()/hide(): Visibility control
- o toggleFullScreen()
- • Handles:
- o 4chan X integration
- o View style switching
- o Drag-and-drop for files
- */
- (function(module, exports) {
- module.exports = {
- atRoot: ['show', 'hide'],
- delegatedEvents: {
- click: {
- [`.${ns}-close-button`]: 'hide'
- },
- fullscreenchange: {
- [`.${ns}-media-and-controls`]: 'display._handleFullScreenChange'
- },
- drop: {
- [`#${ns}-container`]: 'display._handleDrop'
- }
- },
- /**
- * Create the player show/hide button in the 8chan header
- */
- initHeader: function() {
- if (Player.display._initedHeader) {
- return;
- }
- // Find the header navigation container
- const navOptions = document.querySelector('#navOptionsSpan');
- if (!navOptions) {
- return;
- }
- Player.display._initedHeader = true;
- // Create the sounds button
- const soundsButton = createElement(`
- <span>
- <span>/</span>
- <a href="javascript:;" title="Toggle sound player" class="coloredIcon" ">
- [♫]
- </a>
- </span>
- `);
- // Insert before the closing bracket
- navOptions.insertBefore(soundsButton, navOptions.lastElementChild);
- // Add click handler
- soundsButton.querySelector('a').addEventListener('click', Player.display.toggle);
- // Also add to mobile menu
- const mobileMenu = document.querySelector('#sidebar-menu ul');
- if (mobileMenu) {
- const mobileItem = createElement(`
- <li>
- <a href="javascript:;" class="coloredIcon">
- Sound Player
- </a>
- </li>
- `);
- mobileMenu.appendChild(mobileItem);
- mobileItem.querySelector('a').addEventListener('click', Player.display.toggle);
- }
- },
- /**
- * Initialize footer elements
- */
- initFooter: function() {
- if (Player.display._initedFooter) {
- return;
- }
- // Find the footer navigation container
- const threadBottom = document.querySelector('.threadBottom .innerUtility');
- if (!threadBottom) {
- return;
- }
- Player.display._initedFooter = true;
- // Check if sounds link already exists
- if (!threadBottom.querySelector('a[href="javascript:;"][onclick]')) {
- // Create the sounds button
- const soundsButton = createElement(`
- <a href="javascript:;" title="Toggle sound player">Sound Player</a>
- `);
- // Insert after Catalog link
- const catalogLink = threadBottom.querySelector('a[href$="catalog.html"]');
- if (catalogLink) {
- threadBottom.insertBefore(document.createTextNode(' '), catalogLink.nextSibling);
- threadBottom.insertBefore(soundsButton, catalogLink.nextSibling);
- threadBottom.insertBefore(document.createTextNode(' '), catalogLink.nextSibling);
- } else {
- // Fallback if catalog link not found
- threadBottom.insertBefore(document.createTextNode(' '), threadBottom.firstChild);
- threadBottom.insertBefore(soundsButton, threadBottom.firstChild);
- }
- // Add click handler
- soundsButton.addEventListener('click', Player.display.toggle);
- }
- },
- /**
- * Render the player.
- */
- render: async function() {
- try {
- if (Player.container) {
- document.body.removeChild(Player.container);
- document.head.removeChild(Player.stylesheet);
- }
- // Create the main stylesheet.
- Player.display.updateStylesheet();
- // Create the main player. For native threads put it in the threads to get free quote previews.
- const isThread = document.body.classList.contains('is_thread');
- const parent = isThread && !isChanX && document.body.querySelector('.board') || document.body;
- Player.container = createElement(Player.templates.body(), parent);
- Player.trigger('rendered');
- } catch (err) {
- Player.logError('There was an error rendering the sound player. Please check the console for details.');
- console.error('[8chan sounds player]', err);
- // Can't recover, throw.
- throw err;
- }
- },
- updateStylesheet: function() {
- // Insert the stylesheet if it doesn't exist.
- Player.stylesheet = Player.stylesheet || createElement('<style></style>', document.head);
- Player.stylesheet.innerHTML = Player.templates.css();
- },
- /**
- * Change what view is being shown
- */
- setViewStyle: function(style) {
- // Get the size and style prior to switching.
- const previousStyle = Player.config.viewStyle;
- const {
- width,
- height
- } = Player.container.getBoundingClientRect();
- const containerStyle = window.getComputedStyle(document.querySelector(`#${ns}-container`));
- const containerWidth = parseFloat(containerStyle.getPropertyValue('width')) || width;
- // Exit fullscreen before changing to a different view.
- if (style !== 'fullscreen') {
- document.fullscreenElement && document.exitFullscreen();
- }
- // Change the style.
- Player.set('viewStyle', style);
- Player.container.setAttribute('data-view-style', style);
- // Try to reapply the pre change sizing unless it was fullscreen.
- if (previousStyle !== 'fullscreen' || style === 'fullscreen') {
- Player.position.resize(parseInt(containerWidth, 10), parseInt(height, 10));
- }
- Player.trigger('view', style, previousStyle);
- },
- /**
- * Togle the display status of the player.
- */
- toggle: function(e) {
- e && e.preventDefault();
- if (Player.container.style.display === 'none') {
- Player.show();
- } else {
- Player.hide();
- }
- },
- /**
- * Hide the player. Stops polling for changes, and pauses the aduio if set to.
- */
- hide: function(e) {
- if (!Player.container) {
- return;
- }
- try {
- e && e.preventDefault();
- Player.container.style.display = 'none';
- Player.isHidden = true;
- Player.trigger('hide');
- } catch (err) {
- Player.logError('There was an error hiding the sound player. Please check the console for details.');
- console.error('[8chan sounds player]', err);
- }
- },
- /**
- * Show the player. Reapplies the saved position/size, and resumes loaded amount polling if it was paused.
- */
- show: async function(e) {
- if (!Player.container) {
- return;
- }
- try {
- e && e.preventDefault();
- if (!Player.container.style.display) {
- return;
- }
- Player.container.style.display = null;
- Player.isHidden = false;
- await Player.trigger('show');
- } catch (err) {
- Player.logError('There was an error showing the sound player. Please check the console for details.');
- console.error('[8chan sounds player]', err);
- }
- },
- /**
- * Toggle the video/image and controls fullscreen state
- */
- toggleFullScreen: async function() {
- if (!document.fullscreenElement) {
- // Make sure the player (and fullscreen contents) are visible first.
- if (Player.isHidden) {
- Player.show();
- }
- Player.$(`.${ns}-media-and-controls`).requestFullscreen();
- } else if (document.exitFullscreen) {
- document.exitFullscreen();
- }
- },
- /**
- * Handle file/s being dropped on the player.
- */
- _handleDrop: function(e) {
- e.preventDefault();
- e.stopPropagation();
- Player.playlist.addFromFiles(e.dataTransfer.files);
- },
- /**
- * Handle the fullscreen state being changed
- */
- _handleFullScreenChange: function() {
- if (document.fullscreenElement) {
- Player.display.setViewStyle('fullscreen');
- document.querySelector(`.${ns}-image-link`).removeAttribute('href');
- } else {
- if (Player.playing) {
- //document.querySelector(`.${ns}-image-link`).href = Player.playing.image;
- document.querySelector(`.${ns}-image-link`).removeAttribute('href');
- }
- Player.playlist.restore();
- }
- }
- };
- }),
- /* 8 - Event System
- • Custom event bus with:
- o Delegated event handling
- o Audio event bindings
- o Pub/sub pattern (on/off/trigger)
- • Manages all player interactions
- */
- (function(module, exports) {
- module.exports = {
- atRoot: ['on', 'off', 'trigger'],
- // Holder of event handlers.
- _events: {},
- _delegatedEvents: {},
- _undelegatedEvents: {},
- _audioEvents: [],
- initialize: function() {
- const eventLocations = {
- Player,
- ...Player.components
- };
- const delegated = Player.events._delegatedEvents;
- const undelegated = Player.events._undelegatedEvents;
- const audio = Player.events._audioEvents;
- for (let name in eventLocations) {
- const comp = eventLocations[name];
- for (let evt in comp.delegatedEvents || {}) {
- delegated[evt] || (delegated[evt] = []);
- delegated[evt].push(comp.delegatedEvents[evt]);
- }
- for (let evt in comp.undelegatedEvents || {}) {
- undelegated[evt] || (undelegated[evt] = []);
- undelegated[evt].push(comp.undelegatedEvents[evt]);
- }
- comp.audioEvents && (audio.push(comp.audioEvents));
- }
- Player.on('rendered', function() {
- // Wire up delegated events on the container.
- Player.events.addDelegatedListeners(Player.container, delegated);
- // Wire up undelegated events.
- Player.events.addUndelegatedListeners(document, undelegated);
- // Wire up audio events.
- for (let eventList of audio) {
- for (let evt in eventList) {
- Player.audio.addEventListener(evt, Player.events.getHandler(eventList[evt]));
- }
- }
- });
- },
- /**
- * Set delegated events listeners on a target
- */
- addDelegatedListeners(target, events) {
- for (let evt in events) {
- target.addEventListener(evt, function(e) {
- let nodes = [e.target];
- while (nodes[nodes.length - 1] !== target) {
- nodes.push(nodes[nodes.length - 1].parentNode);
- }
- for (let node of nodes) {
- for (let eventList of [].concat(events[evt])) {
- for (let selector in eventList) {
- if (node.matches && node.matches(selector)) {
- e.eventTarget = node;
- let handler = Player.events.getHandler(eventList[selector]);
- // If the handler returns false stop propogation
- if (handler && handler(e) === false) {
- return;
- }
- }
- }
- }
- }
- });
- }
- },
- /**
- * Set, or reset, directly bound events.
- */
- addUndelegatedListeners: function(target, events) {
- for (let evt in events) {
- for (let eventList of [].concat(events[evt])) {
- for (let selector in eventList) {
- target.querySelectorAll(selector).forEach(element => {
- const handler = Player.events.getHandler(eventList[selector]);
- element.removeEventListener(evt, handler);
- element.addEventListener(evt, handler);
- });
- }
- }
- }
- },
- /**
- * Create an event listener on the player.
- *
- * @param {String} evt The name of the events.
- * @param {function} handler The handler function.
- */
- on: function(evt, handler) {
- Player.events._events[evt] || (Player.events._events[evt] = []);
- Player.events._events[evt].push(handler);
- },
- /**
- * Remove an event listener on the player.
- *
- * @param {String} evt The name of the events.
- * @param {function} handler The handler function.
- */
- off: function(evt, handler) {
- const index = Player.events._events[evt] && Player.events._events[evt].indexOf(handler);
- if (index > -1) {
- Player.events._events[evt].splice(index, 1);
- }
- },
- /**
- * Trigger an event on the player.
- *
- * @param {String} evt The name of the events.
- * @param {*} data Data passed to the handler.
- */
- trigger: async function(evt, ...data) {
- const events = Player.events._events[evt] || [];
- for (let handler of events) {
- await handler(...data);
- }
- },
- /**
- * Returns the function of Player referenced by name or a given handler function.
- * @param {String|Function} handler Name to function on Player or a handler function.
- */
- getHandler: function(handler) {
- return typeof handler === 'string' ? _get(Player, handler) : handler;
- }
- };
- }),
- /* 9 - Footer Components
- • Template rendering for:
- o Footer (status info)
- • Uses the user-defined templates
- */
- (function(module, exports) {
- module.exports = {
- initialize: function() {
- Player.userTemplate.maintain(Player.footer, 'footerTemplate');
- },
- render: function() {
- if (Player.container) {
- Player.$(`.${ns}-footer`).innerHTML = Player.templates.footer();
- Player.position.preventWrappingFooter();
- }
- }
- };
- }),
- /* 10 - Header Components
- • Template rendering for:
- o Player header (controls)
- • Uses the user-defined templates
- */
- (function(module, exports) {
- module.exports = {
- initialize: function() {
- Player.userTemplate.maintain(Player.header, 'headerTemplate');
- },
- render: function() {
- if (Player.container) {
- Player.$(`.${ns}-header`).innerHTML = Player.templates.header();
- }
- }
- };
- }),
- /* 11 - Hotkey System
- • Keyboard control:
- o Binding management
- o Key event handling
- o Modifier key support
- • Configurable activation modes
- */
- (function(module, exports, __webpack_require__) {
- const settingsConfig = __webpack_require__(1);
- module.exports = {
- initialize: function() {
- Player.on('rendered', Player.hotkeys.apply);
- },
- _keyMap: {
- ' ': 'space',
- arrowleft: 'left',
- arrowright: 'right',
- arrowup: 'up',
- arrowdown: 'down'
- },
- addHandler: () => {
- Player.hotkeys.removeHandler();
- document.body.addEventListener('keydown', Player.hotkeys.handle);
- },
- removeHandler: () => {
- document.body.removeEventListener('keydown', Player.hotkeys.handle);
- },
- /**
- * Apply the selecting hotkeys option
- */
- apply: function() {
- const type = Player.config.hotkeys;
- Player.hotkeys.removeHandler();
- Player.off('show', Player.hotkeys.addHandler);
- Player.off('hide', Player.hotkeys.removeHandler);
- if (type === 'always') {
- // If hotkeys are always enabled then just set the handler.
- Player.hotkeys.addHandler();
- } else if (type === 'open') {
- // If hotkeys are only enabled with the player toggle the handler as the player opens/closes.
- // If the player is already open set the handler now.
- if (!Player.isHidden) {
- Player.hotkeys.addHandler();
- }
- Player.on('show', Player.hotkeys.addHandler);
- Player.on('hide', Player.hotkeys.removeHandler);
- }
- },
- /**
- * Handle a keydown even on the body
- */
- handle: function(e) {
- // Ignore events on inputs so you can still type.
- const ignoreFor = ['INPUT', 'SELECT', 'TEXTAREA', 'INPUT'];
- if (ignoreFor.includes(e.target.nodeName) || Player.isHidden && (Player.config.hotkeys !== 'always' || !Player.sounds.length)) {
- return;
- }
- const k = e.key.toLowerCase();
- const bindings = Player.config.hotkey_bindings || {};
- // Look for a matching hotkey binding
- for (let key in bindings) {
- const keyDef = bindings[key];
- const bindingConfig = k === keyDef.key &&
- (!!keyDef.shiftKey === !!e.shiftKey) && (!!keyDef.ctrlKey === !!e.ctrlKey) && (!!keyDef.metaKey === !!e.metaKey) &&
- (!keyDef.ignoreRepeat || !e.repeat) &&
- settingsConfig.find(s => s.property === 'hotkey_bindings').settings.find(s => s.property === 'hotkey_bindings.' + key);
- if (bindingConfig) {
- e.preventDefault();
- return _get(Player, bindingConfig.keyHandler)();
- }
- }
- },
- /**
- * Turn a hotkey definition or key event into an input string.
- */
- stringifyKey: function(key) {
- let k = key.key.toLowerCase();
- Player.hotkeys._keyMap[k] && (k = Player.hotkeys._keyMap[k]);
- return (key.ctrlKey ? 'Ctrl+' : '') + (key.shiftKey ? 'Shift+' : '') + (key.metaKey ? 'Meta+' : '') + k;
- },
- /**
- * Turn an input string into a hotkey definition object.
- */
- parseKey: function(str) {
- const keys = str.split('+');
- let key = keys.pop();
- Object.keys(Player.hotkeys._keyMap).find(k => Player.hotkeys._keyMap[k] === key && (key = k));
- const newValue = {
- key
- };
- keys.forEach(key => newValue[key.toLowerCase() + 'Key'] = true);
- return newValue;
- },
- volumeUp: function() {
- Player.audio.volume = Math.min(Player.audio.volume + 0.05, 1);
- },
- volumeDown: function() {
- Player.audio.volume = Math.max(Player.audio.volume - 0.05, 0);
- }
- };
- }),
- /* 12 - Minimized UI
- • Picture-in-picture mode:
- o Thumbnail display
- o 4chan X header controls
- • Handles compact view states
- */
- (function(module, exports) {
- module.exports = {
- _showingPIP: false,
- initialize: function() {
- if (isChanX) {
- // Create a reply element to gather the style from
- const a = createElement('<a></a>', document.body);
- const style = document.defaultView.getComputedStyle(a);
- createElement(`<style>.${ns}-chan-x-controls .${ns}-media-control > div { background: ${style.color} }</style>`, document.head);
- // Clean up the element.
- document.body.removeChild(a);
- // Set up the contents and maintain user template changes.
- Player.userTemplate.maintain(Player.minimised, 'chanXTemplate', ['chanXControls'], ['show', 'hide']);
- }
- Player.on('rendered', Player.minimised.render);
- Player.on('show', Player.minimised.hidePIP);
- Player.on('hide', Player.minimised.showPIP);
- Player.on('playsound', Player.minimised.showPIP);
- },
- render: function() {
- if (Player.container && isChanX) {
- let container = document.querySelector(`.${ns}-chan-x-controls`);
- // Create the element if it doesn't exist.
- // Set the user template and control events on it to make all the buttons work.
- if (!container) {
- container = createElementBefore(`<span class="${ns}-chan-x-controls ${ns}-col-auto"></span>`, document.querySelector('#shortcuts').firstElementChild);
- Player.events.addDelegatedListeners(container, {
- click: [Player.userTemplate.delegatedEvents.click, Player.controls.delegatedEvents.click]
- });
- }
- if (Player.config.chanXControls === 'never' || Player.config.chanXControls === 'closed' && !Player.isHidden) {
- return container.innerHTML = '';
- }
- // Render the contents.
- container.innerHTML = Player.userTemplate.build({
- template: Player.config.chanXTemplate,
- sound: Player.playing,
- replacements: {
- 'prev-button': `<div class="${ns}-media-control ${ns}-previous-button"><div class="${ns}-previous-button-display"></div></div>`,
- '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>`,
- 'next-button': `<div class="${ns}-media-control ${ns}-next-button"><div class="${ns}-next-button-display"></div></div>`,
- 'sound-current-time': `<span class="${ns}-current-time">0:00</span>`,
- 'sound-duration': `<span class="${ns}-duration">0:00</span>`
- }
- });
- }
- },
- /**
- * Move the image to a picture in picture like thumnail.
- */
- showPIP: function() {
- if (!Player.isHidden || !Player.config.pip || !Player.playing || Player.minimised._showingPIP) {
- return;
- }
- Player.minimised._showingPIP = true;
- const image = document.querySelector(`.${ns}-media`);
- document.body.appendChild(image);
- image.classList.add(`${ns}-pip`);
- image.style.bottom = (Player.position.getHeaderOffset().bottom) + 'px';
- // Show the player again when the image is clicked.
- image.addEventListener('click', Player.show);
- },
- /**
- * Move the image back to the player.
- */
- hidePIP: function() {
- Player.minimised._showingPIP = false;
- const image = document.querySelector(`.${ns}-media`);
- Player.$(`.${ns}-media-and-controls`).insertBefore(document.querySelector(`.${ns}-media`), Player.$(`.${ns}-controls`));
- image.classList.remove(`${ns}-pip`);
- image.style.bottom = null;
- image.removeEventListener('click', Player.show);
- }
- };
- }),
- /* 13 - Playlist Management
- • Sound collection:
- o add()/remove()
- o Drag-and-drop reordering
- o Filtering
- • Features:
- o Hover image previews
- o Video detection
- o Playlist navigation
- */
- (function(module, exports, __webpack_require__) {
- const videoFileExtRE = /\.(webm|mp4|m4v|ogv|avi|mpeg|mpg|mpe|m1v|m2v|mov|wmv)$/i;
- const videoMimeRE = /^video\/.+$/;
- const {
- parseFiles,
- parseFileName
- } = __webpack_require__(0);
- module.exports = {
- atRoot: ['add', 'remove'],
- delegatedEvents: {
- click: {
- [`.${ns}-list-item`]: 'playlist.handleSelect',
- [`.${ns}-sound-tag-toggle-button`]: 'playlist.toggleSoundTagPosts'
- },
- mousemove: {
- [`.${ns}-list-item`]: 'playlist.positionHoverImage'
- },
- dragstart: {
- [`.${ns}-list-item`]: 'playlist.handleDragStart'
- },
- dragenter: {
- [`.${ns}-list-item`]: 'playlist.handleDragEnter'
- },
- dragend: {
- [`.${ns}-list-item`]: 'playlist.handleDragEnd'
- },
- dragover: {
- [`.${ns}-list-item`]: e => e.preventDefault()
- },
- drop: {
- [`.${ns}-list-item`]: e => e.preventDefault()
- }
- },
- undelegatedEvents: {
- mouseenter: {
- [`.${ns}-list-item`]: 'playlist.updateHoverImage'
- },
- mouseleave: {
- [`.${ns}-list-item`]: 'playlist.removeHoverImage'
- }
- },
- initialize: function() {
- // Keep track of the last view style so we can return to it.
- Player.playlist._lastView = Player.config.viewStyle === 'playlist' || Player.config.viewStyle === 'image' ?
- Player.config.viewStyle :
- 'playlist';
- Player.on('view', style => {
- // Focus the playing song when switching to the playlist.
- style === 'playlist' && Player.playlist.scrollToPlaying();
- // Track state.
- if (style === 'playlist' || style === 'image') {
- Player.playlist._lastView = style;
- }
- });
- // Update the UI when a new sound plays, and scroll to it.
- Player.on('playsound', sound => {
- Player.playlist.showImage(sound);
- Player.$all(`.${ns}-list-item.playing`).forEach(el => el.classList.remove('playing'));
- Player.$(`.${ns}-list-item[data-id="${Player.playing.id}"]`).classList.add('playing');
- Player.playlist.scrollToPlaying('nearest');
- });
- // Reapply filters when they change
- Player.on('config:filters', Player.playlist.applyFilters);
- // Listen to anything that can affect the display of hover images
- Player.on('config:hoverImages', Player.playlist.setHoverImageVisibility);
- Player.on('menu-open', Player.playlist.setHoverImageVisibility);
- Player.on('menu-close', Player.playlist.setHoverImageVisibility);
- Player.on('config:showSoundTagOnly', Player.playlist.applySoundTagFilter);
- // Maintain changes to the user templates it's dependent values
- Player.userTemplate.maintain(Player.playlist, 'rowTemplate', ['shuffle']);
- },
- /**
- * Render the playlist.
- */
- render: function() {
- if (!Player.container) {
- return;
- }
- const container = Player.$(`.${ns}-list-container`);
- container.innerHTML = Player.templates.list();
- Player.events.addUndelegatedListeners(document.body, Player.playlist.undelegatedEvents);
- Player.playlist.hoverImage = Player.$(`.${ns}-hover-image`);
- Player.playlist.applySoundTagFilter(); // Apply filter after rendering
- },
- /**
- * Restore the last playlist or image view.
- */
- restore: function() {
- Player.display.setViewStyle(Player.playlist._lastView || 'playlist');
- },
- /**
- * Update the image displayed in the player.
- */
- showImage: function(sound, thumb) {
- if (!Player.container) {
- return;
- }
- //let isVideo = Player.playlist.isVideo = !thumb && (sound.image.endsWith('.webm') || sound.image.endsWith('.mp4') || sound.type === 'video/webm' || sound.type === 'video/mp4');
- let isVideo = Player.playlist.isVideo = !thumb && videoFileExtRE.test(sound.image) || videoMimeRE.test(sound.type);
- try {
- const container = document.querySelector(`.${ns}-media`);
- const img = container.querySelector(`.${ns}-image`);
- const video = container.querySelector(`.${ns}-video`);
- // const imageLink = container.querySelector(`.${ns}-image-link`);
- img.src = '';
- img.src = isVideo || thumb ? sound.thumb : sound.image;
- video.src = isVideo ? sound.image : undefined;
- // if (Player.config.viewStyle !== 'fullscreen') {
- // imageLink.href = sound.image;
- // }
- container.classList[isVideo ? 'add' : 'remove'](ns + '-show-video');
- } catch (err) {
- Player.logError('There was an error display the sound player image. Please check the console for details.');
- console.error('[8chan sounds player]', err);
- }
- },
- /**
- * Switch between playlist and image view.
- */
- toggleView: function(e) {
- if (!Player.container) {
- return;
- }
- e && e.preventDefault();
- let style = Player.config.viewStyle === 'playlist' ? 'image' : 'playlist';
- try {
- Player.display.setViewStyle(style);
- } catch (err) {
- Player.logError('There was an error switching the view style. Please check the console for details.', 'warning');
- console.error('[8chan sounds player]', err);
- }
- },
- /**
- * Add a new sound from the thread to the player.
- */
- add: function(sound, skipRender) {
- try {
- const id = sound.id;
- // Make sure the sound is not a duplicate.
- if (Player.sounds.find(sound => sound.id === id)) {
- return;
- }
- // Add the sound with the location based on the shuffle settings.
- let index = Player.config.shuffle ?
- Math.floor(Math.random() * Player.sounds.length - 1) :
- Player.sounds.findIndex(s => Player.compareIds(s.id, id) > 1);
- index < 0 && (index = Player.sounds.length);
- Player.sounds.splice(index, 0, sound);
- if (Player.container) {
- if (!skipRender) {
- // Add the sound to the playlist.
- const list = Player.$(`.${ns}-list-container`);
- let rowContainer = document.createElement('div');
- rowContainer.innerHTML = Player.templates.list({
- sounds: [sound]
- });
- Player.events.addUndelegatedListeners(rowContainer, Player.playlist.undelegatedEvents);
- let row = rowContainer.children[0];
- if (index < Player.sounds.length - 1) {
- const before = Player.$(`.${ns}-list-item[data-id="${Player.sounds[index + 1].id}"]`);
- list.insertBefore(row, before);
- } else {
- list.appendChild(row);
- }
- }
- // If nothing else has been added yet show the image for this sound.
- if (Player.sounds.length === 1) {
- // If we're on a thread with autoshow enabled then make sure the player is displayed
- if (/\/thread\//.test(location.href) && Player.config.autoshow) {
- Player.show();
- }
- Player.playlist.showImage(sound);
- }
- Player.trigger('add', sound);
- Player.playlist.applySoundTagFilter(); // filter new sounds
- }
- } catch (err) {
- Player.logError('There was an error adding to the sound player. Please check the console for details.');
- console.log('[8chan sounds player]', sound);
- console.error('[8chan sounds player]', err);
- }
- },
- /**
- * Add a new local sound from the users computer to the player.
- */
- addFromFiles: async function(files) {
- for (const file of files) {
- // Skip non-media files
- if (!file.type.startsWith('image') && !file.type.startsWith('video/')) {
- console.log("localFile is not an image or video");
- return;
- }
- const filenameRE = /(.*?)[[({](?:sound)[ =:|$](.*?)[\])}]/g;
- if (file.type.startsWith('image') && !filenameRE.test(file.name)) {
- console.log("localFile: image without [sound=URL]");
- return;
- }
- try {
- // Convert file to base64 data URL instead of blob URL
- const dataUrl = await new Promise((resolve) => {
- const reader = new FileReader();
- reader.onload = () => resolve(reader.result);
- reader.readAsDataURL(file);
- });
- const videoFileExtRE = /(webm|mp4|m4v|ogv|avi|mpeg|mpg|mpe|m1v|m2v|mov|wmv)$/i;
- const imageSrc = dataUrl;
- const type = file.type;
- let thumbSrc = imageSrc;
- const fileURL = dataUrl;
- const fileExt = file.name.split('.').pop().toLowerCase();
- const isVideo = videoFileExtRE.test(fileExt);
- if (isVideo) {
- // Create video thumbnail
- const video = document.createElement('video');
- const canvas = document.createElement('canvas');
- const ctx = canvas.getContext('2d');
- await new Promise((resolve) => {
- video.addEventListener('loadeddata', () => {
- canvas.width = video.videoWidth;
- canvas.height = video.videoHeight;
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
- thumbSrc = canvas.toDataURL('image/jpeg');
- resolve();
- });
- video.src = dataUrl;
- video.currentTime = 0.1; // Seek to a small time to get a frame
- });
- }
- function formatFileSize(bytes) {
- if (bytes === 0) return '0 KB';
- const units = ['KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
- // Ensure we never return "Bytes" (always at least KB)
- const adjustedSize = i === 0 ? bytes / 1024 : bytes / Math.pow(1024, i);
- const unit = i === 0 ? 'KB' : units[i - 1];
- return adjustedSize.toFixed(2) + ' ' + unit;
- }
- const fileSize = formatFileSize(file.size);
- //function parseFileName(filename, imageSrc, postNumber, thumbSrc, imageMD5, fileIndex, fileSize, dataFilemime);
- parseFileName(file.name, imageSrc, 'localFile:'+window.localFileCounter, thumbSrc, null, 'lF'+window.localFileCounter, fileSize, file.type)
- .forEach(sound => Player.add({
- ...sound,
- id: 'localFile:' + window.localFileCounter,
- local: true,
- type,
- }));
- window.localFileCounter++;
- } catch (error) {
- console.error('Error processing file:', file.name, error);
- }
- }
- },
- /**
- * Remove a sound
- */
- remove: function(sound) {
- const index = Player.sounds.indexOf(sound);
- // If the playing sound is being removed then play the next sound.
- if (Player.playing === sound) {
- Player.pause();
- Player.next(true);
- }
- // Remove the sound from the the list and play order.
- if (index > -1) {
- Player.sounds.splice(index, 1);
- // Clean up blob URLs only for local files
- if (sound.local) {
- if (sound.url?.startsWith('blob:')) URL.revokeObjectURL(sound.url);
- if (sound.image?.startsWith('blob:')) URL.revokeObjectURL(sound.image);
- if (sound.thumb?.startsWith('blob:')) URL.revokeObjectURL(sound.thumb);
- }
- }
- // Remove the item from the list.
- Player.$(`.${ns}-list-container`).removeChild(Player.$(`.${ns}-list-item[data-id="${sound.id}"]`));
- Player.trigger('remove', sound);
- },
- /**
- * Handle an playlist item being clicked. Either open/close the menu or play the sound.
- */
- handleSelect: function(e) {
- // Ignore if a link was clicked.
- if (e.target.nodeName === 'A' || e.target.closest('a')) {
- return;
- }
- e.preventDefault();
- const id = e.eventTarget.getAttribute('data-id');
- const sound = id && Player.sounds.find(sound => sound.id === id);
- sound && Player.play(sound);
- },
- /**
- * Read all the sounds from the thread again.
- */
- refresh: function() {
- parseFiles(document.body);
- },
- /**
- * Toggle the hoverImages setting
- */
- toggleHoverImages: function(e) {
- e && e.preventDefault();
- Player.set('hoverImages', !Player.config.hoverImages);
- },
- /**
- * Only show the hover image with the setting enabled, no item menu open, and nothing being dragged.
- */
- setHoverImageVisibility: function() {
- const container = Player.$(`.${ns}-player`);
- const hideImage = !Player.config.hoverImages ||
- Player.playlist._dragging ||
- container.querySelector(`.${ns}-item-menu`);
- container.classList[hideImage ? 'add' : 'remove'](`${ns}-hide-hover-image`);
- },
- /**
- * Set the displayed hover image and reposition.
- */
- updateHoverImage: function(e) {
- const id = e.currentTarget.getAttribute('data-id');
- const sound = Player.sounds.find(sound => sound.id === id);
- Player.playlist.hoverImage.style.display = 'block';
- Player.playlist.hoverImage.setAttribute('src', sound.thumb);
- Player.playlist.positionHoverImage(e);
- },
- /**
- * Reposition the hover image to follow the cursor.
- */
- positionHoverImage: function(e) {
- const {
- width,
- height
- } = Player.playlist.hoverImage.getBoundingClientRect();
- const maxX = document.documentElement.clientWidth - width - 5;
- Player.playlist.hoverImage.style.left = (Math.min(e.clientX, maxX) + 5) + 'px';
- Player.playlist.hoverImage.style.top = (e.clientY - height - 10) + 'px';
- },
- /**
- * Hide the hover image when nothing is being hovered over.
- */
- removeHoverImage: function() {
- Player.playlist.hoverImage.style.display = 'none';
- },
- /**
- * Start dragging a playlist item.
- */
- handleDragStart: function(e) {
- Player.playlist._dragging = e.eventTarget;
- Player.playlist.setHoverImageVisibility();
- e.eventTarget.classList.add(`${ns}-dragging`);
- e.dataTransfer.setDragImage(new Image(), 0, 0);
- e.dataTransfer.dropEffect = 'move';
- e.dataTransfer.setData('text/plain', e.eventTarget.getAttribute('data-id'));
- },
- /**
- * Swap a playlist item when it's dragged over another item.
- */
- handleDragEnter: function(e) {
- if (!Player.playlist._dragging) {
- return;
- }
- e.preventDefault();
- const moving = Player.playlist._dragging;
- const id = moving.getAttribute('data-id');
- let before = e.target.closest && e.target.closest(`.${ns}-list-item`);
- if (!before || moving === before) {
- return;
- }
- const movingIdx = Player.sounds.findIndex(s => s.id === id);
- const list = moving.parentNode;
- // If the item is being moved down it need inserting before the node after the one it's dropped on.
- const position = moving.compareDocumentPosition(before);
- if (position & 0x04) {
- before = before.nextSibling;
- }
- // Move the element and sound.
- // If there's nothing to go before then append.
- if (before) {
- const beforeId = before.getAttribute('data-id');
- const beforeIdx = Player.sounds.findIndex(s => s.id === beforeId);
- const insertIdx = movingIdx < beforeIdx ? beforeIdx - 1 : beforeIdx;
- list.insertBefore(moving, before);
- Player.sounds.splice(insertIdx, 0, Player.sounds.splice(movingIdx, 1)[0]);
- } else {
- Player.sounds.push(Player.sounds.splice(movingIdx, 1)[0]);
- list.appendChild(moving);
- }
- Player.trigger('order');
- },
- /**
- * Start dragging a playlist item.
- */
- handleDragEnd: function(e) {
- if (!Player.playlist._dragging) {
- return;
- }
- e.preventDefault();
- delete Player.playlist._dragging;
- e.eventTarget.classList.remove(`${ns}-dragging`);
- Player.playlist.setHoverImageVisibility();
- },
- /**
- * Scroll to the playing item, unless there is an open menu in the playlist.
- */
- scrollToPlaying: function(type = 'center') {
- if (Player.$(`.${ns}-list-container .${ns}-item-menu`)) {
- return;
- }
- const playing = Player.$(`.${ns}-list-item.playing`);
- playing && playing.scrollIntoView({
- block: type
- });
- },
- /**
- * Remove any user filtered items from the playlist.
- */
- applyFilters: function() {
- Player.sounds.filter(sound => !Player.acceptedSound(sound)).forEach(Player.playlist.remove);
- },
- toggleSoundTagPosts: function(e) {
- e && e.preventDefault();
- Player.set('showSoundTagOnly', !Player.config.showSoundTagOnly);
- Player.playlist.applySoundTagFilter();
- },
- applySoundTagFilter: function() {
- const showSoundTagOnly = Player.config.showSoundTagOnly;
- // Update button text
- const buttons = document.querySelectorAll(`.${ns}-sound-tag-toggle-button`);
- buttons.forEach(button => {
- button.title = showSoundTagOnly ? 'Show all posts' : 'Show only posts with sound tag';
- });
- // Filter playlist items
- const items = Player.$all(`.${ns}-list-item`);
- items.forEach(item => {
- const id = item.getAttribute('data-id');
- const sound = Player.sounds.find(s => s.id === id);
- if (sound) {
- item.style.display = showSoundTagOnly && !sound.hasSoundTag ? 'none' : '';
- }
- });
- }
- };
- }),
- /* 14 - Positioning
- • Player window:
- o Draggable header
- o Resizable
- o Smart post width limiting
- • Handles:
- o Saved position/size
- o Viewport constraints
- o 4chan X header offsets
- */
- (function(module, exports) {
- module.exports = {
- delegatedEvents: {
- mousedown: {
- [`.${ns}-header`]: 'position.initMove',
- [`.${ns}-expander`]: 'position.initResize'
- }
- },
- initialize: function() {
- // Apply the last position/size, and post width limiting, when the player is shown.
- Player.on('show', async function() {
- const [top, left] = (await GM.getValue('position') || '').split(':');
- const [width, height] = (await GM.getValue('size') || '').split(':'); +
- top && +left && Player.position.move(top, left, true); +
- width && +height && Player.position.resize(width, height);
- // Ensure player is on screen when shown
- Player.position.ensureOnScreen();
- if (Player.config.limitPostWidths) {
- Player.position.setPostWidths();
- window.addEventListener('scroll', Player.position.setPostWidths);
- }
- });
- // Remove post width limiting when the player is hidden.
- Player.on('hide', function() {
- Player.position.setPostWidths();
- window.removeEventListener('scroll', Player.position.setPostWidths);
- });
- // Reapply the post width limiting config values when they're changed.
- Player.on('config', prop => {
- if (prop === 'limitPostWidths' || prop === 'minPostWidth') {
- window.removeEventListener('scroll', Player.position.setPostWidths);
- Player.position.setPostWidths();
- if (Player.config.limitPostWidths) {
- window.addEventListener('scroll', Player.position.setPostWidths);
- }
- }
- });
- // Remove post width limit from inline quotes
- /*new MutationObserver(function() {
- document.querySelectorAll('#hoverUI .postContainer, .inline .postContainer, .backlink_container article').forEach(post => {
- post.style.maxWidth = null;
- post.style.minWidth = null;
- });
- }).observe(document.body, {
- childList: true,
- subtree: true
- });*/
- this.debouncedResize = window.debounceFc(() => {
- if (Player.config.limitPostWidths) {
- Player.position.setPostWidths();
- }
- Player.position.preventWrapping();
- Player.position.preventWrappingFooter();
- }, 8);
- window.addEventListener('resize', this.debouncedResize);
- // Document resize observer
- this.resizeObserver = new ResizeObserver(entries => {
- if (Player.container && !Player.isHidden) {
- Player.position.ensureOnScreen();
- }
- });
- this.resizeObserver.observe(document.documentElement);
- this.resizeObserver.observe(document.body);
- // Listen for changes from other tabs
- Player.syncTab('position', value => Player.position.move(...value.split(':').concat(true)));
- Player.syncTab('size', value => Player.position.resize(...value.split(':')));
- Player.on("config:preventControlsWrapping", (e) => !e && Player.position.showAllControls());
- Player.on("config:controlsHideOrder", () => {
- Player.position.setHideOrder();
- Player.position.preventWrapping();
- });
- },
- /**
- * Applies a max width to posts next to the player so they don't get hidden behind it.
- */
- setPostWidths: window.throttleFc(function() {
- const offset = (document.documentElement.clientWidth - Player.container.offsetLeft) + 10;
- const selector = '.innerPost';
- const enabled = !Player.isHidden && Player.config.limitPostWidths;
- const startY = Player.container.offsetTop;
- const endY = Player.container.getBoundingClientRect().height + startY;
- document.querySelectorAll(selector).forEach(post => {
- const rect = enabled && post.getBoundingClientRect();
- const limitWidth = enabled && rect.top + rect.height > startY && rect.top < endY;
- post.style.maxWidth = limitWidth ? `calc(100% - ${offset}px)` : null;
- post.style.minWidth = limitWidth && Player.config.minPostWidth ? `${Player.config.minPostWidth}` : null;
- });
- }, 100),
- /**
- * Handle the user grabbing the expander.
- */
- initResize: function initDrag(e) {
- e.preventDefault();
- Player._startX = e.clientX;
- Player._startY = e.clientY;
- let {
- width,
- height
- } = Player.container.getBoundingClientRect();
- Player._startWidth = width;
- Player._startHeight = height;
- document.documentElement.addEventListener('mousemove', Player.position.doResize, false);
- document.documentElement.addEventListener('mouseup', Player.position.stopResize, false);
- },
- /**
- * Handle the user dragging the expander.
- */
- doResize: function(e) {
- e.preventDefault();
- Player.position.resize(Player._startWidth + e.clientX - Player._startX, Player._startHeight + e.clientY - Player._startY);
- },
- /**
- * Handle the user releasing the expander.
- */
- stopResize: function() {
- const {
- width,
- height
- } = Player.container.getBoundingClientRect();
- document.documentElement.removeEventListener('mousemove', Player.position.doResize, false);
- document.documentElement.removeEventListener('mouseup', Player.position.stopResize, false);
- GM.setValue('size', width + ':' + height);
- },
- /**
- * Resize the player.
- */
- resize: function(width, height) {
- if (!Player.container || Player.config.viewStyle === 'fullscreen') {
- return;
- }
- const {
- bottom
- } = Player.position.getHeaderOffset();
- // Make sure the player isn't going off screen.
- height = Math.min(height, document.documentElement.clientHeight - Player.container.offsetTop - bottom);
- width = Math.min(Math.ceil(width), document.documentElement.clientWidth - Player.container.offsetLeft);
- Player.container.style.width = width + 'px';
- // Which element to change the height of depends on the view being displayed.
- const heightElement = Player.config.viewStyle === 'playlist' ? Player.$(`.${ns}-list-container`) :
- Player.config.viewStyle === 'image' ? Player.$(`.${ns}-media`) :
- Player.config.viewStyle === 'settings' ? Player.$(`.${ns}-settings`) :
- Player.config.viewStyle === 'threads' ? Player.$(`.${ns}-threads`) : null;
- if (!heightElement) {
- return;
- }
- const offset = Player.container.getBoundingClientRect().height - heightElement.getBoundingClientRect().height;
- heightElement.style.height = (height - offset) + 'px';
- // Check control wrapping after resize
- Player.position.preventWrapping();
- Player.position.preventWrappingFooter();
- },
- /**
- * Handle the user grabbing the header.
- */
- initMove: function(e) {
- e.preventDefault();
- Player.$(`.${ns}-header`).style.cursor = 'grabbing';
- // Try to reapply the current sizing to fix oversized winows.
- const {
- width,
- height
- } = Player.container.getBoundingClientRect();
- const containerStyle = window.getComputedStyle(document.querySelector(`#${ns}-container`));
- const containerWidth = parseFloat(containerStyle.getPropertyValue('width')) || width;
- Player.position.resize(containerWidth, height);
- Player._offsetX = e.clientX - Player.container.offsetLeft;
- Player._offsetY = e.clientY - Player.container.offsetTop;
- document.documentElement.addEventListener('mousemove', Player.position.doMove, false);
- document.documentElement.addEventListener('mouseup', Player.position.stopMove, false);
- },
- /**
- * Handle the user dragging the header.
- */
- doMove: function(e) {
- e.preventDefault();
- Player.position.move(e.clientX - Player._offsetX, e.clientY - Player._offsetY);
- },
- /**
- * Handle the user releasing the header.
- */
- stopMove: function() {
- document.documentElement.removeEventListener('mousemove', Player.position.doMove, false);
- document.documentElement.removeEventListener('mouseup', Player.position.stopMove, false);
- Player.$(`.${ns}-header`).style.cursor = null;
- GM.setValue('position', parseInt(Player.container.style.left, 10) + ':' + parseInt(Player.container.style.top, 10));
- },
- /**
- * Move the player.
- */
- move: function(x, y, allowOffscreen) {
- if (!Player.container) {
- return;
- }
- const {
- top,
- bottom
- } = Player.position.getHeaderOffset();
- // Ensure the player stays fully within the window.
- const {
- width,
- height
- } = Player.container.getBoundingClientRect();
- const maxX = allowOffscreen ? Infinity : document.documentElement.clientWidth - width;
- const maxY = allowOffscreen ? Infinity : document.documentElement.clientHeight - height - bottom;
- // Move the window.
- Player.container.style.left = Math.max(0, Math.min(x, maxX)) + 'px';
- Player.container.style.top = Math.max(top, Math.min(y, maxY)) + 'px';
- if (Player.config.limitPostWidths) {
- Player.position.setPostWidths();
- }
- },
- /**
- * Get the offset from the top or bottom required for the 4chan X header.
- */
- getHeaderOffset: function() {
- const top = 26;
- const bottom = 0;
- return {
- top,
- bottom
- };
- },
- /**
- * Ensures the player is within the visible screen area
- */
- ensureOnScreen: function() {
- if (!Player.container || Player.isHidden || Player.config.viewStyle === 'fullscreen') {
- return;
- }
- const containerRect = Player.container.getBoundingClientRect();
- const viewportWidth = document.documentElement.clientWidth;
- const viewportHeight = document.documentElement.clientHeight;
- const { top: headerTop, bottom: headerBottom } = this.getHeaderOffset();
- // Check if player is completely offscreen
- const isOffscreen =
- containerRect.right < 0 ||
- containerRect.bottom < headerTop ||
- containerRect.left > viewportWidth ||
- containerRect.top > viewportHeight - headerBottom;
- if (isOffscreen) {
- // Move to default position if completely offscreen
- this.move(10, headerTop + 10);
- } else {
- // Adjust position if partially offscreen
- let newLeft = containerRect.left;
- let newTop = containerRect.top;
- if (containerRect.left < 0) {
- newLeft = 0;
- } else if (containerRect.right > viewportWidth) {
- newLeft = viewportWidth - containerRect.width;
- }
- if (containerRect.top < headerTop) {
- newTop = headerTop;
- } else if (containerRect.bottom > viewportHeight - headerBottom) {
- newTop = viewportHeight - headerBottom - containerRect.height;
- }
- if (newLeft !== containerRect.left || newTop !== containerRect.top) {
- this.move(newLeft, newTop);
- }
- }
- },
- showAllControls: function() {
- Player.$all(`.${ns}-controls [data-hide-id]`).forEach((e) => (e.style.display = null));
- },
- preventWrapping: function() {
- // Reset display style first
- Player.position.showAllControls();
- if (!Player.config.preventControlWrapping) return;
- const container = Player.$(`.${ns}-controls`);
- const hideOrder = Player.position.setHideOrder();
- let controls = Array.from(container.children).filter(el => el.hasAttribute('data-hide-id'));
- let lastControl = controls[controls.length - 1];
- // Get initial state
- const containerWidth = container.clientWidth;
- let contentWidth = Array.from(container.children).reduce((sum, el) => sum + el.clientWidth, 0);
- if (contentWidth <= containerWidth) return;
- // Hide controls until content fits
- let hideIndex = 0;
- while (contentWidth > containerWidth && hideIndex < hideOrder.length) {
- const controlToHide = hideOrder[hideIndex];
- if (!controlToHide) continue;
- controlToHide.style.display = "none";
- controls = controls.filter(control => control !== controlToHide);
- if (controlToHide === lastControl && controls.length > 0) {
- lastControl = controls[controls.length - 1];
- }
- contentWidth = Array.from(container.children).reduce((sum, el) => sum + el.clientWidth, 0);
- hideIndex++;
- }
- },
- setHideOrder: function() {
- // Reset to default if not set
- if (!Array.isArray(Player.config.controlsHideOrder)) {
- Player.settings.reset("controlsHideOrder");
- }
- const controlsContainer = Player.$(`.${ns}-controls`);
- // Create priority map based on array position
- const priorityMap = {};
- Player.config.controlsHideOrder.forEach((control, index) => {
- priorityMap[control] = index;
- });
- // Get all hideable controls, filter to only those in priorityMap, and sort by priority
- Player.position.hideOrder = Array.from(controlsContainer.querySelectorAll('[data-hide-id]'))
- .filter(element => element.getAttribute('data-hide-id') in priorityMap)
- .sort((a, b) => {
- const aPriority = priorityMap[a.getAttribute('data-hide-id')];
- const bPriority = priorityMap[b.getAttribute('data-hide-id')];
- return aPriority - bPriority;
- });
- return Player.position.hideOrder;
- },
- preventWrappingFooter: function() {
- const container = Player.$(`.${ns}-footer`);
- if (!container) return;
- const containerWidth = container.clientWidth;
- const uiBrackets = document.querySelectorAll(`.${ns}-ui-bracket`);
- const footerText = document.querySelectorAll(`.${ns}-footer-text`);
- // Hide or unhide
- const bracketDisplay = containerWidth < 225 ? "none" : "";
- const textDisplay = containerWidth < 345 ? "none" : "";
- uiBrackets.forEach(el => el.style.display = bracketDisplay);
- footerText.forEach(el => el.style.display = textDisplay);
- },
- };
- }),
- /* 15 - Thread Search
- • Catalog scanning:
- o Board selection
- o Sound thread detection
- • Displays:
- o Table view (metadata)
- o Board-style view (4chan X only)
- */
- (function(module, exports, __webpack_require__) {
- const {
- parseFileName
- } = __webpack_require__(0);
- const {
- get
- } = __webpack_require__(16);
- const boardsURL = /*'https://a.4cdn.org/boards.json'*/'';
- const catalogURL = /*'https://a.4cdn.org/%s/catalog.json'*/'';
- module.exports = {
- boardList: null,
- soundThreads: null,
- displayThreads: {},
- selectedBoards: Board ? [Board] : ['a'],
- showAllBoards: false,
- delegatedEvents: {
- click: {
- [`.${ns}-fetch-threads-link`]: 'threads.fetch',
- [`.${ns}-all-boards-link`]: 'threads.toggleBoardList'
- },
- keyup: {
- [`.${ns}-threads-filter`]: e => Player.threads.filter(e.eventTarget.value)
- },
- change: {
- [`.${ns}-threads input[type=checkbox]`]: 'threads.toggleBoard'
- }
- },
- initialize: function() {
- Player.threads.hasParser = is4chan && typeof Parser !== 'undefined';
- // If the native Parser hasn't been intialised chuck customSpoiler on it so we can call it for threads.
- // You shouldn't do things like this. We can fall back to the table view if it breaks though.
- if (Player.threads.hasParser && !Parser.customSpoiler) {
- Parser.customSpoiler = {};
- }
- Player.on('show', Player.threads._initialFetch);
- Player.on('view', Player.threads._initialFetch);
- Player.on('rendered', Player.threads.afterRender);
- Player.on('config:threadsViewStyle', Player.threads.render);
- },
- /**
- * Fetch the threads when the threads view is opened for the first time.
- */
- _initialFetch: function() {
- if (Player.container && Player.config.viewStyle === 'threads' && Player.threads.boardList === null) {
- Player.threads.fetchBoards(true);
- }
- },
- render: function() {
- if (Player.container) {
- Player.$(`.${ns}-threads`).innerHTML = Player.templates.threads();
- Player.threads.afterRender();
- }
- },
- /**
- * Render the threads and apply the board styling after the view is rendered.
- */
- afterRender: function() {
- const threadList = Player.$(`.${ns}-thread-list`);
- if (threadList) {
- const bodyStyle = document.defaultView.getComputedStyle(document.body);
- threadList.style.background = bodyStyle.backgroundColor;
- threadList.style.backgroundImage = bodyStyle.backgroundImage;
- threadList.style.backgroundRepeat = bodyStyle.backgroundRepeat;
- threadList.style.backgroundPosition = bodyStyle.backgroundPosition;
- }
- Player.threads.renderThreads();
- },
- /**
- * Render just the threads.
- */
- renderThreads: function() {
- if (!Player.threads.hasParser || Player.config.threadsViewStyle === 'table') {
- Player.$(`.${ns}-threads-body`).innerHTML = Player.templates.threadList();
- } else {
- try {
- const list = Player.$(`.${ns}-thread-list`);
- for (let board in Player.threads.displayThreads) {
- // Create a board title
- const boardConf = Player.threads.boardList.find(boardConf => boardConf.board === board);
- const boardTitle = `/${boardConf.board}/ - ${boardConf.title}`;
- createElement(`<div class="boardBanner"><div class="boardTitle">${boardTitle}</div></div>`, list);
- // Add each thread for the board
- const threads = Player.threads.displayThreads[board];
- for (let i = 0; i < threads.length; i++) {
- list.appendChild(Parser.buildHTMLFromJSON.call(Parser, threads[i], threads[i].board, true, true));
- // Add a line under each thread
- createElement('<hr style="clear: both">', list);
- }
- }
- } catch (err) {
- Player.logError('Unable to display the threads board view.', 'warning');
- // If there was an error fall back to the table view.
- Player.set('threadsViewStyle', 'table');
- Player.renderThreads();
- }
- }
- },
- /**
- * Render just the board selection.
- */
- renderBoards: function() {
- Player.$(`.${ns}-thread-board-list`).innerHTML = Player.templates.threadBoards();
- },
- /**
- * Toggle the threads view.
- */
- toggle: function(e) {
- e && e.preventDefault();
- if (Player.config.viewStyle === 'threads') {
- Player.playlist.restore();
- } else {
- Player.display.setViewStyle('threads');
- }
- },
- /**
- * Switch between showing just the selected boards and all boards.
- */
- toggleBoardList: function() {
- Player.threads.showAllBoards = !Player.threads.showAllBoards;
- Player.$(`.${ns}-all-boards-link`).innerHTML = Player.threads.showAllBoards ? 'Selected Only' : 'Show All';
- Player.threads.renderBoards();
- },
- /**
- * Select/deselect a board.
- */
- toggleBoard: function(e) {
- const board = e.eventTarget.value;
- const selected = e.eventTarget.checked;
- if (selected) {
- !Player.threads.selectedBoards.includes(board) && Player.threads.selectedBoards.push(board);
- } else {
- Player.threads.selectedBoards = Player.threads.selectedBoards.filter(b => b !== board);
- }
- },
- /**
- * Fetch the board list from the 4chan API.
- */
- fetchBoards: async function(fetchThreads) {
- Player.threads.loading = true;
- Player.threads.render();
- Player.threads.boardList = (await get(boardsURL)).boards;
- if (fetchThreads) {
- Player.threads.fetch();
- } else {
- Player.threads.loading = false;
- Player.threads.render();
- }
- },
- /**
- * Fetch the catalog for each selected board and search for sounds in OPs.
- */
- fetch: async function(e) {
- e && e.preventDefault();
- Player.threads.loading = true;
- Player.threads.render();
- if (!Player.threads.boardList) {
- try {
- await Player.threads.fetchBoards();
- } catch (err) {
- Player.logError('Failed to fetch the boards configuration.');
- console.error(err);
- return;
- }
- }
- const allThreads = [];
- try {
- await Promise.all(Player.threads.selectedBoards.map(async board => {
- const boardConf = Player.threads.boardList.find(boardConf => boardConf.board === board);
- if (!boardConf) {
- return;
- }
- const pages = boardConf && await get(catalogURL.replace('%s', board));
- (pages || []).forEach(({
- page,
- threads
- }) => {
- allThreads.push(...threads.map(thread => Object.assign(thread, {
- board,
- page,
- ws_board: boardConf.ws_board
- })));
- });
- }));
- Player.threads.soundThreads = allThreads.filter(thread => {
- const sounds = parseFileName(thread.filename, `https://i.4cdn.org/${thread.board}/${thread.tim}${thread.ext}`, thread.no, `https://i.4cdn.org/${thread.board}/${thread.tim}s${thread.ext}`, thread.md5);
- return sounds.length;
- });
- } catch (err) {
- Player.logError('Failed to search for sounds threads.');
- console.error(err);
- }
- Player.threads.loading = false;
- Player.threads.filter(Player.$(`.${ns}-threads-filter`).value, true);
- Player.threads.render();
- },
- /**
- * Apply the filter input to the already fetched threads.
- */
- filter: function(search, skipRender) {
- Player.threads.filterValue = search || '';
- if (Player.threads.soundThreads === null) {
- return;
- }
- Player.threads.displayThreads = Player.threads.soundThreads.reduce((threadsByBoard, thread) => {
- if (!search || thread.sub && thread.sub.includes(search) || thread.com && thread.com.includes(search)) {
- threadsByBoard[thread.board] || (threadsByBoard[thread.board] = []);
- threadsByBoard[thread.board].push(thread);
- }
- return threadsByBoard;
- }, {});
- !skipRender && Player.threads.renderThreads();
- }
- };
- }),
- /* 16 - Network Utilities
- • Cached requests:
- o get(): GM_xmlHttpRequest wrapper
- o Conditional requests
- o JSON handling
- */
- (function(module, exports) {
- const cache = {};
- module.exports = {
- get
- };
- async function get(url) {
- return new Promise(function(resolve, reject) {
- const headers = {};
- if (cache[url]) {
- headers['If-Modified-Since'] = cache[url].lastModified;
- }
- GM.xmlHttpRequest({
- method: 'GET',
- url,
- headers,
- responseType: 'json',
- onload: response => {
- if (response.status >= 200 && response.status < 300) {
- cache[url] = {
- lastModified: response.responseHeaders['last-modified'],
- response: response.response
- };
- }
- resolve(response.status === 304 ? cache[url].response : response.response);
- },
- onerror: reject
- });
- });
- }
- }),
- /* 17 - Template System
- • Dynamic UI generation:
- o Button definitions
- o Template parsing
- o Conditional rendering
- • Handles all user-customizable layouts
- */
- (function(module, exports, __webpack_require__) {
- const buttons = __webpack_require__(18);
- // Regex for replacements
- const playingRE = /p: ?{([^}]*)}/g;
- const hoverRE = /h: ?{([^}]*)}/g;
- const buttonRE = new RegExp(`(${buttons.map(option => option.tplName).join('|')})-(?:button|link|icon)(?:\\:"([^"]+?)")?`, 'g');
- const soundNameRE = /sound-name/g;
- const soundIndexRE = /sound-index/g;
- const soundCountRE = /sound-count/g;
- // Hold information on which config values components templates depend on.
- const componentDeps = [];
- module.exports = {
- buttons,
- delegatedEvents: {
- click: {
- [`.${ns}-playing-jump-link`]: () => Player.playlist.scrollToPlaying('center'),
- [`.${ns}-viewStyle-button`]: 'playlist.toggleView',
- [`.${ns}-hoverImages-button`]: 'playlist.toggleHoverImages',
- [`.${ns}-remove-link`]: 'userTemplate._handleRemove',
- [`.${ns}-filter-link`]: 'userTemplate._handleFilter',
- [`.${ns}-download-link`]: 'userTemplate._handleDownload',
- [`.${ns}-shuffle-button`]: 'userTemplate._handleShuffle',
- [`.${ns}-repeat-button`]: 'userTemplate._handleRepeat',
- [`.${ns}-reload-button`]: noDefault('playlist.refresh'),
- [`.${ns}-add-button`]: noDefault(() => Player.$(`.${ns}-file-input`).click()),
- [`.${ns}-item-menu-button`]: 'userTemplate._handleMenu',
- [`.${ns}-threads-button`]: 'threads.toggle',
- [`.${ns}-config-button`]: 'settings.toggle'
- },
- change: {
- [`.${ns}-file-input`]: 'userTemplate._handleFileSelect'
- }
- },
- undelegatedEvents: {
- click: {
- body: 'userTemplate._closeMenus'
- },
- keydown: {
- body: e => e.key === 'Escape' && Player.userTemplate._closeMenus()
- }
- },
- initialize: function() {
- Player.on('config', Player.userTemplate._handleConfig);
- Player.on('playsound', () => Player.userTemplate._handleEvent('playsound'));
- Player.on('add', () => Player.userTemplate._handleEvent('add'));
- Player.on('remove', () => Player.userTemplate._handleEvent('remove'));
- Player.on('order', () => Player.userTemplate._handleEvent('order'));
- Player.on('show', () => Player.userTemplate._handleEvent('show'));
- Player.on('hide', () => Player.userTemplate._handleEvent('hide'));
- },
- /**
- * Build a user template.
- */
- build: function(data) {
- const outerClass = data.outerClass || '';
- const name = (data.template === Player.config.headerTemplate)
- ? (data.sound && data.sound.post || data.defaultName)
- : (data.sound && data.sound.title || data.defaultName);
- const postID = data.sound && data.sound.post || data.defaultName;
- // Apply common template replacements
- let html = data.template
- .replace(playingRE, Player.playing && Player.playing === data.sound ? '$1' : '')
- .replace(hoverRE, `<span class="${ns}-hover-display ${outerClass}">$1</span>`)
- .replace(buttonRE, function(full, type, text) {
- let buttonConf = buttons.find(conf => conf.tplName === type);
- if (buttonConf.requireSound && !data.sound || buttonConf.showIf && !buttonConf.showIf(data)) {
- return '';
- }
- // If the button config has sub values then extend the base config with the selected sub value.
- // Which value is to use is taken from the `property` in the base config of the player config.
- // This gives us different state displays.
- if (buttonConf.values) {
- buttonConf = {
- ...buttonConf,
- ...buttonConf.values[_get(Player.config, buttonConf.property)] || buttonConf.values[Object.keys(buttonConf.values)[0]]
- };
- }
- const attrs = typeof buttonConf.attrs === 'function' ? buttonConf.attrs(data) : buttonConf.attrs || [];
- attrs.some(attr => attr.startsWith('href')) || attrs.push('href=javascript:;');
- (buttonConf.class || outerClass) && attrs.push(`class="${buttonConf.class || ''} ${outerClass || ''}"`);
- if (!text) {
- text = buttonConf.icon ?
- `<svg xmlns="http://www.w3.org/2000/svg" ${buttonConf.icon}></svg>`+buttonConf.text :
- buttonConf.text;
- }
- if (/-icon$/.test(full)) return `<div ${attrs.join(' ')}>${text}</div>`;
- return `<a ${attrs.join(' ')} draggable="false">${text}</a>`;
- })
- .replace(soundNameRE, name ? `<div class="fc-sounds-col fc-sounds-truncate-text"><span title="${postID}">${name}</span></div>` : '')
- .replace(soundIndexRE, data.sound ? Player.sounds.indexOf(data.sound) + 1 : 0)
- .replace(soundCountRE, Player.sounds.length)
- .replace(/%v/g, "2.3.0");
- // Apply any specific replacements
- if (data.replacements) {
- for (let k of Object.keys(data.replacements)) {
- html = html.replace(new RegExp(k, 'g'), data.replacements[k]);
- }
- }
- return html;
- },
- /**
- * Sets up a components to render when the template or values within it are changed.
- */
- maintain: function(component, property, alwaysRenderConfigs = [], alwaysRenderEvents = []) {
- componentDeps.push({
- component,
- property,
- ...Player.userTemplate.findDependencies(property, null),
- alwaysRenderConfigs,
- alwaysRenderEvents
- });
- },
- /**
- * Find all the config dependent values in a template.
- */
- findDependencies: function(property, template) {
- template || (template = _get(Player.config, property));
- // Figure out what events should trigger a render.
- const events = [];
- // add/remove should render templates showing the count.
- // playsound should render templates showing the playing sounds name/index or dependent on something playing.
- // order should render templates showing a sounds index.
- const hasCount = soundCountRE.test(template);
- const hasName = soundNameRE.test(template);
- const hasIndex = soundIndexRE.test(template);
- const hasPlaying = playingRE.test(template);
- hasCount && events.push('add', 'remove');
- (hasPlaying || property !== 'rowTemplate' && (hasName || hasIndex)) && events.push('playsound');
- hasIndex && events.push('order');
- // Find which buttons the template includes that are dependent on config values.
- const config = [];
- let match;
- while ((match = buttonRE.exec(template)) !== null) {
- // If user text is given then the display doesn't change.
- if (!match[2]) {
- let type = match[1];
- let buttonConf = buttons.find(conf => conf.tplName === type);
- if (buttonConf.property) {
- config.push(buttonConf.property);
- }
- }
- }
- return {
- events,
- config
- };
- },
- /**
- * When a config value is changed check if any component dependencies are affected.
- */
- _handleConfig: function(property, value) {
- // Check if a template for a components was updated.
- componentDeps.forEach(depInfo => {
- if (depInfo.property === property) {
- Object.assign(depInfo, Player.userTemplate.findDependencies(property, value));
- depInfo.component.render();
- }
- });
- // Check if any components are dependent on the updated property.
- componentDeps.forEach(depInfo => {
- if (depInfo.alwaysRenderConfigs.includes(property) || depInfo.config.includes(property)) {
- depInfo.component.render();
- }
- });
- },
- /**
- * When a player event is triggered check if any component dependencies are affected.
- */
- _handleEvent: function(type) {
- // Check if any components are dependent on the updated property.
- componentDeps.forEach(depInfo => {
- if (depInfo.alwaysRenderEvents.includes(type) || depInfo.events.includes(type)) {
- depInfo.component.render();
- }
- });
- },
- /**
- * Add local files.
- */
- _handleFileSelect: function(e) {
- e.preventDefault();
- const input = e.eventTarget;
- Player.playlist.addFromFiles(input.files);
- },
- /**
- * Toggle the repeat style.
- */
- _handleRepeat: function(e) {
- try {
- e.preventDefault();
- const values = ['all', 'one', 'none'];
- const current = values.indexOf(Player.config.repeat);
- Player.set('repeat', values[(current + 4) % 3]);
- } catch (err) {
- Player.logError('There was an error changing the repeat setting. Please check the console for details.', 'warning');
- console.error('[8chan sounds player]', err);
- }
- },
- /**
- * Toggle the shuffle style.
- */
- _handleShuffle: function(e) {
- try {
- e.preventDefault();
- Player.set('shuffle', !Player.config.shuffle);
- Player.header.render();
- // Update the play order.
- if (!Player.config.shuffle) {
- Player.sounds.sort((a, b) => Player.compareIds(a.id, b.id));
- } else {
- const sounds = Player.sounds;
- for (let i = sounds.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [sounds[i], sounds[j]] = [sounds[j], sounds[i]];
- }
- }
- Player.trigger('order');
- } catch (err) {
- Player.logError('There was an error changing the shuffle setting. Please check the console for details.', 'warning');
- console.error('[8chan sounds player]', err);
- }
- },
- /**
- * Display an item menu.
- */
- _handleMenu: function(e) {
- e.preventDefault();
- e.stopPropagation();
- const x = e.clientX;
- const y = e.clientY;
- const id = e.eventTarget.getAttribute('data-id');
- const sound = Player.sounds.find(s => s.id === id);
- // Add row item menus to the list container. Append to the container otherwise.
- const listContainer = e.eventTarget.closest(`.${ns}-list-container`);
- const parent = listContainer || Player.container;
- // Create the menu.
- const dialog = createElement(Player.templates.itemMenu({
- x,
- y,
- sound
- }), parent);
- parent.appendChild(dialog);
- // Make sure it's within the page.
- const style = document.defaultView.getComputedStyle(dialog);
- const width = parseInt(style.width, 10);
- const height = parseInt(style.height, 10);
- // Show the dialog to the left of the cursor, if there's room.
- if (x - width > 0) {
- dialog.style.left = x - width + 'px';
- }
- // Move the dialog above the cursor if it's off screen.
- if (y + height > document.documentElement.clientHeight - 40) {
- dialog.style.top = y - height + 'px';
- }
- // Add the focused class handler
- dialog.querySelectorAll('.entry').forEach(el => {
- el.addEventListener('mouseenter', Player.userTemplate._setFocusedMenuItem);
- el.addEventListener('mouseleave', Player.userTemplate._unsetFocusedMenuItem);
- });
- Player.trigger('menu-open', dialog);
- },
- /**
- * Close any open menus, except for one belonging to an item that was clicked.
- */
- _closeMenus: function() {
- document.querySelectorAll(`.${ns}-item-menu`).forEach(menu => {
- menu.parentNode.removeChild(menu);
- Player.trigger('menu-close', menu);
- });
- },
- _setFocusedMenuItem: function(e) {
- e.currentTarget.classList.add('focused');
- const submenu = e.currentTarget.querySelector('.submenu');
- // Move the menu to the other side if there isn't room.
- if (submenu && submenu.getBoundingClientRect().right > document.documentElement.clientWidth) {
- submenu.style.inset = '0px auto auto -100%';
- }
- },
- _unsetFocusedMenuItem: function(e) {
- e.currentTarget.classList.remove('focused');
- },
- _handleFilter: function(e) {
- e.preventDefault();
- let filter = e.eventTarget.getAttribute('data-filter');
- if (filter) {
- Player.set('filters', Player.config.filters.concat(filter));
- }
- },
- _handleDownload: function(e) {
- const src = e.eventTarget.getAttribute('data-src');
- const name = e.eventTarget.getAttribute('data-name') || new URL(src).pathname.split('/').pop();
- GM.xmlHttpRequest({
- method: 'GET',
- url: src,
- responseType: 'blob',
- onload: response => {
- const a = createElement(`<a href="${URL.createObjectURL(response.response)}" download="${name}" rel="noopener" target="_blank"></a>`);
- a.click();
- URL.revokeObjectURL(a.href);
- },
- onerror: () => Player.logError('There was an error downloading.', 'warning')
- });
- },
- _handleRemove: function(e) {
- const id = e.eventTarget.getAttribute('data-id');
- const sound = id && Player.sounds.find(sound => sound.id === '' + id);
- sound && Player.remove(sound);
- },
- };
- }),
- /* 18 - Button Definitions
- • All control buttons:
- o Icons
- o Behavior flags
- o State variants
- • Organized by function (playback, navigation, etc.)
- */
- (function(module, exports) {
- module.exports = [{
- property: 'repeat',
- tplName: 'repeat',
- class: `${ns}-ui-button ${ns}-repeat-button`,
- values: {
- all: {
- attrs: ['title="Repeat All"'],
- text: '',
- 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" />'
- },
- one: {
- attrs: ['title="Repeat One"'],
- text: '',
- 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" />'
- },
- none: {
- attrs: ['title="No Repeat"'],
- text: '',
- 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" />'
- }
- }
- },
- {
- property: 'shuffle',
- tplName: 'shuffle',
- class: `${ns}-ui-button ${ns}-shuffle-button`,
- values: {
- true: {
- attrs: ['title="Shuffled"'],
- text: '',
- 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" />',
- },
- false: {
- attrs: ['title="Ordered"'],
- text: '',
- 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" />',
- }
- }
- },
- {
- property: 'viewStyle',
- tplName: 'playlist',
- class: `${ns}-ui-button ${ns}-viewStyle-button`,
- values: {
- playlist: {
- attrs: ['title="Show Playlist Enabled"'],
- text: '',
- 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" />',
- },
- image: {
- attrs: ['title="Show Playlist Disabled"'],
- text: '',
- 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" />',
- }
- }
- },
- {
- property: 'hoverImages',
- tplName: 'hover-images',
- class: `${ns}-ui-button ${ns}-hoverImages-button`,
- values: {
- true: {
- attrs: ['title="Hover Images Enabled"'],
- text: '',
- 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" />',
- },
- false: {
- attrs: ['title="Hover Images Disabled"'],
- text: '',
- 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" />',
- }
- }
- },
- {
- tplName: 'add',
- class: `${ns}-ui-button ${ns}-add-button`,
- 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" />',
- text: '',
- attrs: ['title="Add local files"'],
- },
- {
- tplName: 'reload',
- class: `${ns}-ui-button ${ns}-reload-button`,
- 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" />',
- text: '',
- attrs: ['title="Reload the playlist"'],
- },
- {
- tplName: 'settings',
- class: `${ns}-ui-button ${ns}-config-button`,
- 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" />',
- text: '',
- attrs: ['title="Settings"'],
- },
- {
- tplName: 'threads',
- class: `${ns}-ui-button ${ns}-threads-button`,
- 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" />',
- text: '',
- attrs: ['title="Threads"'],
- },
- {
- tplName: 'close',
- class: `${ns}-ui-button ${ns}-close-button`,
- 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" />',
- text: '',
- attrs: ['title="Hide the player"'],
- },
- {
- tplName: 'playing',
- requireSound: true,
- class: `${ns}-ui-button ${ns}-playing-jump-link`,
- text: 'Playing',
- attrs: ['title="Scroll the playlist currently playing sound."'],
- },
- {
- tplName: 'post',
- class: `${ns}-ui-button ${ns}-post-button`,
- requireSound: true,
- 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" />',
- text: '',
- showIf: data => data.sound.post,
- attrs: data => [
- `href=${'#' + (is4chan ? 'p' : '') + data.sound.post}`,
- 'title="Jump to the post for the current sound"',
- ],
- },
- {
- tplName: 'image',
- class: `${ns}-ui-button ${ns}-image-button`,
- requireSound: true,
- 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" />',
- text: '',
- attrs: data => [
- `href=${data.sound.image}`,
- 'title="Open the image in a new tab"',
- 'target="_blank"',
- ],
- },
- {
- tplName: 'sound',
- class: `${ns}-ui-button ${ns}-sound-button`,
- requireSound: true,
- href: data => data.sound.src,
- 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" />',
- text: '',
- attrs: data => [
- `href=${data.sound.src}`,
- 'title="Open the sound in a new tab"',
- 'target="blank"',
- ],
- },
- {
- tplName: 'dl-image',
- requireSound: true,
- class: `${ns}-ui-button ${ns}-download-link`,
- 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" />',
- text: '',
- attrs: data => [
- 'title="Download the image with the original filename"',
- `data-src="${data.sound.image}"`,
- `data-name="${data.sound.filename}"`,
- ],
- },
- {
- tplName: 'dl-sound',
- requireSound: true,
- class: `${ns}-ui-button ${ns}-download-link`,
- 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" />',
- text: '',
- attrs: data => [
- 'title="Download the sound"',
- `data-src="${data.sound.src}"`,
- ],
- },
- {
- tplName: 'filter-image',
- requireSound: true,
- class: `${ns}-ui-button ${ns}-ui-button ${ns}-filter-link`,
- 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" />',
- text: '',
- showIf: data => data.sound.imageMD5,
- attrs: data => [
- 'title="Add the image MD5 to the filters."',
- `data-filter="${data.sound.imageMD5}"`,
- ],
- },
- {
- tplName: 'filter-sound',
- requireSound: true,
- class: `${ns}-ui-button ${ns}-filter-link`,
- 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" />',
- text: '',
- attrs: data => [
- 'title="Add the sound URL to the filters."',
- `data-filter="${data.sound.src.replace(/^(https?:)?\/\//, '')}"`,
- ],
- },
- {
- tplName: 'remove',
- requireSound: true,
- class: `${ns}-ui-button ${ns}-remove-link`,
- 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" />',
- text: '',
- attrs: data => [
- 'title="Filter the image."',
- `data-id="${data.sound.id}"`,
- ],
- },
- {
- tplName: 'menu',
- requireSound: true,
- class: `${ns}-ui-button ${ns}-item-menu-button`,
- icon: '',
- text: '▼',
- attrs: data => [`data-id=${data.sound.id}`],
- },
- {
- tplName: 'ui-bracketL',
- class: `${ns}-ui-icon ${ns}-ui-bracket ${ns}-ui-bracketL-icon`,
- 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" />',
- text: '',
- },
- {
- tplName: 'ui-bracketR',
- class: `${ns}-ui-icon ${ns}-ui-bracket ${ns}-ui-bracketR-icon`,
- 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" />',
- text: '',
- },
- {
- tplName: 'ui-files',
- class: `${ns}-ui-icon ${ns}-ui-files-icon`,
- 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" />',
- text: '',
- attrs: data => [
- 'title="Files"',
- ],
- },
- {
- tplName: 'sound-tag-toggle',
- class: `${ns}-ui-button ${ns}-sound-tag-toggle-button`,
- property: 'showSoundTagOnly',
- values: {
- true: {
- attrs: ['title="Show all posts"'],
- text: '',
- 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" />',
- },
- false: {
- attrs: ['title="Show only sound posts"'],
- text: '',
- 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" />',
- }
- }
- },
- ];
- }),
- /* 19 - Templates
- Main player structure */
- (function(module, exports) {
- module.exports = (data = {}) => `<div id="${ns}-container" data-view-style="${Player.config.viewStyle}" style="top: 30px; left: 0px; width: 350px; display: none;">
- <div class="${ns}-header ${ns}-row">
- ${Player.templates.header(data)}
- </div>
- <div class="${ns}-view-container">
- <div class="${ns}-player ${!Player.config.hoverImages ? `${ns}-hide-hover-image` : ''}" ">
- ${Player.templates.player(data)}
- </div>
- <div class="${ns}-settings ${ns}-panel" style="height: 400px">
- ${Player.templates.settings(data)}
- </div>
- <div class="${ns}-threads ${ns}-panel" style="height: 400px">
- ${Player.templates.threads(data)}
- </div>
- </div>
- <div class="${ns}-footer">
- ${Player.templates.footer(data)}
- </div>
- <input class="${ns}-file-input" type="file" style="display: none" accept="image/*,.webm,.mp4" multiple>
- </div>`
- }),
- /* 20 - Templates
- Control bars */
- (function(module, exports) {
- module.exports = (data = {}) => `<div class="${ns}-col-auto" style="padding: 0 0 0 0.25rem;">
- <div class="${ns}-media-control ${ns}-previous-button" data-hide-id="previous">
- <div class="${ns}-previous-button-display"></div>
- </div>
- <div class="${ns}-media-control ${ns}-play-button">
- <div class="${ns}-play-button-display ${!Player.audio || Player.audio.paused ? `${ns}-play` : ''}"></div>
- </div>
- <div class="${ns}-media-control ${ns}-next-button" data-hide-id="next">
- <div class="${ns}-next-button-display"></div>
- </div>
- </div>
- <div class="${ns}-col" data-hide-id="seek-bar">
- <div class="${ns}-seek-bar ${ns}-progress-bar">
- <div class="${ns}-full-bar">
- <div class="${ns}-loaded-bar"></div>
- <div class="${ns}-current-bar"></div>
- </div>
- </div>
- </div>
- <div class="${ns}-col-auto" data-hide-id="time" style="margin: 0 auto; padding: 0 0.25rem;">
- <span class="${ns}-current-time">0:00</span> <span class="${ns}-duration" data-hide-id="duration">/0:00</span>
- </div>
- <div class="${ns}-col-auto" data-hide-id="volume-bar">
- <div class="${ns}-volume-bar ${ns}-progress-bar">
- <div class="${ns}-full-bar">
- <div class="${ns}-current-bar" style="width: ${Player.audio.volume * 100}%"></div>
- </div>
- </div>
- </div>
- <div class="${ns}-col-auto" data-hide-id="fullscreen" style="margin: 0 auto;">
- <div class="${ns}-media-control ${ns}-fullscreen-button">
- <div class="${ns}-fullscreen-button-display"></div>
- </div>
- </div>
- <div class="${ns}-col-auto" style="padding: 0 0.25rem 0 0;"">
- </div>`
- }),
- /* 21 - Templates
- CSS */
- (function(module, exports) {
- module.exports = (data = {}) => `
- /*
- *
- * CONTROLS CSS
- *
- */
- .${ns}-controls {
- align-items: center;
- padding: 0.5rem 0;
- position: relative;
- justify-content: space-between;
- background: ${Player.config.colors.controls_panel};
- border-top: solid ${Player.config.borderWidth} ${Player.config.colors.border};
- border-bottom: solid ${Player.config.borderWidth} ${Player.config.colors.border};
- }
- .${ns}-media-control {
- height: 1.5rem;
- width: 1.5rem;
- display: flex;
- justify-content: center;
- align-items: center;
- cursor: pointer
- }
- .${ns}-media-control .${ns}-col-auto {
- padding: 0 0.5rem;
- }
- .${ns}-media-control>div {
- height: 1rem;
- width: .8rem;
- background: ${Player.config.colors.buttons_color};
- }
- .${ns}-media-control:hover>div {
- background: ${Player.config.colors.hover_color};
- }
- .${ns}-play-button-display {
- clip-path: polygon(10% 10%, 10% 90%, 35% 90%, 35% 10%, 65% 10%, 65% 90%, 90% 90%, 90% 10%, 10% 10%)
- }
- .${ns}-play-button-display.${ns}-play {
- clip-path: polygon(0 0, 0 100%, 100% 50%, 0 0)
- }
- .${ns}-previous-button-display,
- .${ns}-next-button-display {
- clip-path: polygon(10% 10%, 10% 90%, 30% 90%, 30% 50%, 90% 90%, 90% 10%, 30% 50%, 30% 10%, 10% 10%)
- }
- .${ns}-next-button-display {
- transform: scale(-1, 1)
- }
- .${ns}-fullscreen-button-display {
- width: 1rem !important;
- 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%)
- }
- .${ns}-controls .${ns}-current-time {
- font-size: 14px;
- color: ${Player.config.colors.controls_current_time};
- }
- .${ns}-duration {
- font-size: 14px;
- color: ${Player.config.colors.controls_duration};
- }
- .${ns}-progress-bar {
- min-width: 3.5rem;
- height: 1.5rem;
- display: flex;
- align-items: center;
- margin: 0 1rem
- }
- .${ns}-progress-bar .${ns}-full-bar {
- height: .3rem;
- width: 100%;
- background: ${Player.config.colors.progress_bar};
- border-radius: 1rem;
- position: relative
- }
- .${ns}-progress-bar .${ns}-full-bar>div {
- position: absolute;
- top: 0;
- bottom: 0;
- border-radius: 1rem
- }
- .${ns}-progress-bar .${ns}-full-bar .${ns}-loaded-bar {
- background: ${Player.config.colors.progress_bar_loaded};
- }
- .${ns}-progress-bar .${ns}-full-bar .${ns}-current-bar {
- background: ${Player.config.colors.buttons_color};
- display: flex;
- justify-content: flex-end;
- align-items: center
- }
- .${ns}-progress-bar .${ns}-full-bar .${ns}-current-bar:after {
- content: "";
- background: ${Player.config.colors.buttons_color};
- height: .8rem;
- min-width: .8rem;
- border-radius: 1rem;
- box-shadow: rgba(0, 0, 0, .76) 0 0 3px 0
- }
- .${ns}-progress-bar:hover .${ns}-current-bar:after {
- background: ${Player.config.colors.hover_color};
- }
- .${ns}-seek-bar .${ns}-current-bar {
- background: ${Player.config.colors.hover_color};
- }
- .${ns}-volume-bar .${ns}-current-bar {
- background: ${Player.config.colors.controls_current_time};
- }
- .${ns}-chan-x-controls {
- align-items: inherit
- }
- .${ns}-chan-x-controls .${ns}-current-time,
- .${ns}-chan-x-controls .${ns}-duration {
- margin: 0 .25rem
- }
- .${ns}-chan-x-controls .${ns}-media-control {
- width: 1rem;
- height: auto;
- margin-top: -1px
- }
- .${ns}-chan-x-controls .${ns}-media-control>div {
- height: .7rem;
- width: .5rem
- }
- /*
- *
- * FOOTER CSS
- *
- */
- .${ns}-footer {
- padding: .15rem .25rem;
- border-top: solid ${Player.config.borderWidth} ${Player.config.colors.border};
- font-size: 13px;
- }
- .${ns}-footer .${ns}-expander {
- position: absolute;
- bottom: 0px;
- right: 0px;
- height: .75rem;
- width: .75rem;
- cursor: se-resize;
- 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%)
- }
- .${ns}-footer .${ns}-expander:hover {
- 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%)
- }
- .${ns}-footer:hover .${ns}-hover-display {
- display: inline-block
- }
- .${ns}-footer .${ns}-footer-right {
- float: right;
- margin-right: 0.25rem;
- display: flex;
- justify-content: center; /* Horizontal center */
- align-items: center; /* Vertical center */
- text-align: center; /* Optional: center text inside the box */
- }
- .${ns}-footer .${ns}-footer-left {
- float: left;
- display: flex;
- justify-content: center; /* Horizontal center */
- align-items: center; /* Vertical center */
- text-align: center; /* Optional: center text inside the box */
- }
- /*
- *
- * HEADER CSS
- *
- */
- .${ns}-header {
- cursor: grab;
- text-align: center;
- border-bottom:solid ${Player.config.borderWidth} ${Player.config.colors.border};
- padding: .25rem;
- }
- .${ns}-header:hover .${ns}-hover-display {
- display: flex
- }
- .${ns}-header.${ns}-row .${ns}-col.${ns}-truncate-text {
- display: flex;
- justify-content: center; /* Horizontal center */
- align-items: center; /* Vertical center */
- text-align: center; /* Optional: center text inside the box */
- font-size: calc(${Player.config.fontSize}px);
- }
- html.fourchan-x .fa-repeat.fa-repeat-one::after {
- content: "1";
- font-size: .5rem;
- visibility: visible;
- margin-left: -1px
- }
- /*
- *
- * UI CSS
- *
- */
- .${ns}-ui-button {
- color:${Player.config.colors.buttons_color} !important;
- }
- .${ns}-ui-button:hover {
- color:${Player.config.colors.hover_color} !important;
- }
- .${ns}-ui-icon {
- color:${Player.config.colors.text} !important;
- }
- .${ns}-ui-icon:hover {
- color:${Player.config.colors.text} !important;
- }
- /*
- *
- * IMAGE CSS
- *
- */
- #${ns}-container[data-view-style=fc-sounds-playing] .${ns}-view-container .${ns}-media:not(.${ns}-pip),
- #${ns}-container[data-view-style=playlist] .${ns}-view-container .${ns}-media:not(.${ns}-pip),
- #${ns}-container[data-view-style=image] .${ns}-view-container .${ns}-media:not(.${ns}-pip) {
- text-align: center;
- display: flex;
- justify-items: center;
- justify-content: center;
- position: relative;
- resize: both;
- overflow: hidden;
- min-height: ${Player.config.minMediaHeight} !important;
- max-height: ${Player.config.maxMediaHeight} !important;
- min-width: 100%;
- max-width: 100%;
- }
- #${ns}-container[data-view-style=fullscreen] .${ns}-view-container .${ns}-media:not(.${ns}-pip) {
- text-align: center;
- display: flex;
- justify-items: center;
- justify-content: center;
- position: relative;
- resize: both;
- overflow: hidden;
- min-width: 100%;
- max-width: 100%;
- }
- .${ns}-media.${ns}-pip {
- text-align: right;
- position: fixed !important;
- right: ${Player.config.offsetRightPIP} !important;
- bottom: ${Player.config.offsetBottomPIP} !important;
- left: auto !important;
- top: auto !important;
- height: ${Player.config.maxPIPHeight} !important;
- width: ${Player.config.maxPIPWidth} !important;
- z-index: ${Player.config.zIndexPIP};
- }
- .${ns}-media.${ns}-pip .${ns}-image,
- .${ns}-media.${ns}-pip .${ns}-video {
- height: initial;
- width: initial;
- object-fit: contain;
- position: fixed !important;
- right: ${Player.config.offsetRightPIP} !important;
- bottom: ${Player.config.offsetBottomPIP} !important;
- left: auto !important;
- top: auto !important;
- max-height: ${Player.config.maxPIPHeight} !important;
- max-width: ${Player.config.maxPIPWidth} !important;
- }
- .${ns}-media .${ns}-video {
- display: none
- }
- .${ns}-image,
- .${ns}-video {
- object-fit: contain
- }
- .${ns}-media.${ns}-show-video .${ns}-video {
- display: block
- }
- .${ns}-media.${ns}-show-video .${ns}-image {
- display: none
- }
- .${ns}-media img,
- .${ns}-media video {
- object-fit: contain;
- pointer-events: none; /* Disable clicks on the link */
- }
- .${ns}-resize-handle {
- position: absolute;
- right: 0;
- bottom: 0;
- width: 5px;
- height: 5px;
- cursor: se-resize;
- /*z-index: 3;*/
- }
- .${ns}-image-link {
- display: block;
- position: absolute;
- width: 80% !important;
- height: 94% !important;
- opacity: 0;
- }
- .${ns}-media.${ns}-pip .${ns}-image-link {
- display: block;
- position: absolute;
- width: 100% !important;
- height: 100% !important;
- opacity: 0;
- }
- /*
- *
- * LAYOUT CSS
- *
- */
- #${ns}-container {
- position: fixed;
- background:${Player.config.colors.background};
- border: solid ${Player.config.borderWidth} ${Player.config.colors.border};
- min-width: 168px;
- width: 375px;
- color:${Player.config.colors.text};
- }
- .${ns}-panel {
- padding: 0 .25rem;
- height: 100%;
- width: calc(100% - .5rem);
- overflow: auto
- }
- .${ns}-heading {
- font-weight: 600;
- margin: .5rem 0;
- min-width: 100%
- }
- .${ns}-has-description {
- cursor: help
- }
- .${ns}-heading-action {
- font-weight: normal;
- text-decoration: underline;
- margin-left: .25rem
- }
- .${ns}-row {
- display: flex;
- flex-wrap: wrap;
- min-width: 100%;
- box-sizing: border-box
- }
- .${ns}-col-auto {
- flex: 0 0 auto;
- width: auto;
- max-width: 100%;
- display: inline-flex
- }
- .${ns}-col {
- flex-basis: 0;
- flex-grow: 1;
- max-width: 100%;
- width: 100%
- }
- html.fourchan-x #${ns}-container .icon {
- font-size: 0;
- visibility: hidden;
- margin: 0 .15rem
- }
- .${ns}-truncate-text {
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden
- }
- .${ns}-hover-display {
- display: none
- }
- /*
- *
- * LIST CSS
- *
- */
- .${ns}-player .${ns}-hover-image {
- position: fixed;
- max-height: 125px;
- max-width: 125px
- }
- .${ns}-player.${ns}-hide-hover-image .${ns}-hover-image {
- display: none !important
- }
- .${ns}-list-container {
- overflow-y: auto;
- height: 200px;
- font-size: calc(${Player.config.fontSize}px)
- }
- .${ns}-list-container .${ns}-list-item {
- list-style-type: none;
- padding: .15rem .25rem;
- white-space: nowrap;
- text-overflow: ellipsis;
- cursor: pointer;
- background:${Player.config.colors.odd_row};
- overflow: hidden;
- height: calc(${Player.config.fontSize * 0.1}rem);
- font-size: calc(${Player.config.fontSize}px)
- }
- .${ns}-list-container .${ns}-list-item.playing {
- background: ${Player.config.colors.playing} !important;
- color: ${Player.config.colors.text_playing} !important
- }
- .${ns}-list-container .${ns}-list-item:nth-child(2n) {
- background:${Player.config.colors.even_row}
- }
- .${ns}-list-container .${ns}-list-item .${ns}-item-menu-button {
- right: .25rem
- }
- .${ns}-list-container .${ns}-list-item:hover .${ns}-hover-display {
- display: flex
- }
- .${ns}-list-container .${ns}-list-item.${ns}-dragging {
- background: ${Player.config.colors.dragging} !important;
- color: ${Player.config.colors.text_playing} !important
- }
- html:not(.fourchan-x) .dialog {
- background:${Player.config.colors.background};
- background:${Player.config.colors.background};
- border-color:${Player.config.colors.border};
- border-radius: 3px;
- box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
- border-radius: 3px;
- padding-top: 1px;
- padding-bottom: 3px
- }
- html:not(.fourchan-x) .${ns}-item-menu .entry {
- position: relative;
- display: block;
- padding: .125rem .5rem;
- min-width: 70px;
- white-space: nowrap
- }
- html:not(.fourchan-x) .${ns}-item-menu .has-submenu::after {
- content: "";
- border-left: .5em solid;
- border-top: .3em solid transparent;
- border-bottom: .3em solid transparent;
- display: inline-block;
- margin: .35em;
- position: absolute;
- right: 3px
- }
- html:not(.fourchan-x) .${ns}-item-menu .submenu {
- position: absolute;
- display: none
- }
- html:not(.fourchan-x) .${ns}-item-menu .focused>.submenu {
- display: block
- }
- .${ns}-list-container .${ns}-list-item .${ns}-col.${ns}-truncate-text {
- background: transparent !important
- }
- .${ns}-list-container .${ns}-list-item .${ns}-col.${ns}-truncate-text span {
- background: transparent !important
- }
- .${ns}-playlist-file-ext {
- display: inline-block;
- min-width: calc(${Player.config.fontSize * 4}px);
- text-align: left;
- background: transparent !important;
- }
- /*
- *
- * SETTINGS CSS
- *
- */
- .${ns}-settings textarea {
- border: solid ${Player.config.borderWidth} ${Player.config.colors.border};
- min-width: 100%;
- min-height: 4rem;
- box-sizing: border-box;
- white-space: pre
- }
- .${ns}-settings .${ns}-sub-settings .${ns}-col {
- min-height: 1.55rem;
- display: flex;
- align-items: center;
- align-content: center;
- white-space: nowrap
- }
- .${ns}-settings .${ns}-heading-action {
- font-size: 12px;
- }
- .${ns}-settings .${ns}-col {
- font-size: 16px;
- }
- .${ns}-settings .${ns}-col select {
- font-size: 12px;
- }
- .${ns}-settings .${ns}-heading {
- font-size: 19px;
- }
- .${ns}-settings .${ns}-heading::before {
- content: "";
- display: block;
- border-top: solid ${Player.config.borderWidth};
- opacity: 0.2;
- margin-bottom: 0.7em;
- width: 100%;
- }
- /*
- *
- * THREADS CSS
- *
- */
- .${ns}-threads .${ns}-thread-board-list label {
- display: inline-block;
- width: 4rem
- }
- .${ns}-threads .${ns}-thread-list {
- margin: 1rem -0.25rem 0;
- padding: .5rem 1rem;
- border-top:solid ${Player.config.borderWidth} ${Player.config.colors.border}
- }
- .${ns}-threads .${ns}-thread-list .boardBanner {
- margin: 1rem 0
- }
- .${ns}-threads table {
- margin-top: .5rem;
- border-collapse: collapse
- }
- .${ns}-threads table th {
- border-bottom:solid ${Player.config.borderWidth} ${Player.config.colors.border}
- }
- .${ns}-threads table th,
- .${ns}-threads table td {
- text-align: left;
- padding: .25rem
- }
- .${ns}-threads table tr {
- padding: .25rem 0
- }
- .${ns}-threads table .${ns}-threads-body tr {
- background:${Player.config.colors.even_row}
- }
- .${ns}-threads table .${ns}-threads-body tr:nth-child(2n) {
- background:${Player.config.colors.odd_row}
- }
- .${ns}-threads,
- .${ns}-settings,
- .${ns}-player {
- display: none
- }
- #${ns}-container[data-view-style=settings] .${ns}-settings {
- display: block
- }
- #${ns}-container[data-view-style=threads] .${ns}-threads {
- display: block
- }
- #${ns}-container[data-view-style=image] .${ns}-player,
- #${ns}-container[data-view-style=playlist] .${ns}-player,
- #${ns}-container[data-view-style=fullscreen] .${ns}-player {
- display: block
- }
- #${ns}-container[data-view-style=image] .${ns}-list-container {
- display: none
- }
- #${ns}-container[data-view-style=image] .${ns}-media {
- height: auto
- }
- #${ns}-container[data-view-style=playlist] .${ns}-media {
- height: 125px
- }
- #${ns}-container[data-view-style=fullscreen] .${ns}-media {
- height: calc(100% - .4rem) !important
- }
- #${ns}-container[data-view-style=fullscreen] .${ns}-controls {
- position: absolute;
- left: 0;
- right: 0;
- bottom: calc(-2.5rem + .4rem)
- }
- #${ns}-container[data-view-style=fullscreen] .${ns}-controls:hover {
- bottom: 0
- }
- `
- }),
- /* 22 - Templates
- Footer */
- (function(module, exports) {
- module.exports = (data = {}) => Player.userTemplate.build({
- template: Player.config.footerTemplate,
- sound: Player.playing
- }) +
- `<div class="${ns}-expander"></div>`
- }),
- /* 23 - Templates
- Header */
- (function(module, exports) {
- module.exports = (data = {}) => Player.userTemplate.build({
- template: Player.config.headerTemplate,
- sound: Player.playing,
- defaultName: '8chan Sounds',
- outerClass: `${ns}-col-auto`
- });
- }),
- /* 24 - Templates
- Context menus */
- (function(module, exports) {
- 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;">
- <a class="${ns}-remove-link entry focused" href="javascript:;" data-id="${data.sound.id}">Remove</a>
- ${data.sound.post ? `<a class="entry" href="#${(is4chan ? 'p' : '') + data.sound.post}">Show Post</a>` : ''}
- <div class="entry has-submenu">
- Open
- <div class="dialog submenu" style="inset: 0px auto auto 100%;">
- <a class="entry" href="${data.sound.image}" target="_blank">Image</a>
- <a class="entry" href="${data.sound.src}" target="_blank">Sound</a>
- </div>
- </div>
- <div class="entry has-submenu">
- Download
- <div class="dialog submenu" style="inset: 0px auto auto 100%;">
- <a class="${ns}-download-link entry" href="javascript:;" data-src="${data.sound.image}" data-name="${data.sound.filename}">Image</a>
- <a class="${ns}-download-link entry" href="javascript:;" data-src="${data.sound.src}">Sound</a>
- </div>
- </div>
- <div class="entry has-submenu">
- Filter
- <div class="dialog submenu" style="inset: 0px auto auto 100%;">
- ${data.sound.imageMD5 ? `<a class="${ns}-filter-link entry" href="javascript:;" data-filter="${data.sound.imageMD5}">Image</a>` : ''}
- <a class="${ns}-filter-link entry" href="javascript:;" data-filter="${data.sound.src.replace(/^(https?\:)?\/\//, '')}">Sound</a>
- </div>
- </div>
- </div>`
- }),
- /* 25 - Templates
- Playlist items */
- (function(module, exports) {
- module.exports = (data = {}) => (data.sounds || Player.sounds).map(sound =>
- `<div class="${ns}-list-item ${ns}-row ${sound.playing ? 'playing' : ''}" data-id="${sound.id}" draggable="true">
- ${Player.userTemplate.build({
- template: Player.config.rowTemplate,
- sound,
- outerClass: `${ns}-col-auto`
- })}
- </div>`
- ).join('')
- }),
- /* 26 - Templates
- Media display */
- (function(module, exports) {
- module.exports = (data = {}) => `<div class="${ns}-media-and-controls">
- <div class="${ns}-media">
- <a class="${ns}-image-link" target="_blank"></a>
- <img class="${ns}-image"></img>
- <video class="${ns}-video"></video>
- </div>
- <div class="${ns}-controls ${ns}-row">
- ${Player.templates.controls(data)}
- </div>
- </div>
- <div class="${ns}-list-container style="height: 100px">
- ${Player.templates.list(data)}
- </div>
- <img class="${ns}-hover-image">`
- }),
- /* 27 - Templates
- Settings panel */
- (function(module, exports, __webpack_require__) {
- module.exports = (data = {}) => {
- const settingsConfig = __webpack_require__(1);
- let tpl = `
- <div style="text-align: right; font-size: 10px; font-weight: 600; margin: .5rem 0; min-width: 100%"><b>Version</b>
- <a href="https://greasyfork.org/en/scripts/533468-8chan-sounds-player" target="_blank">${VERSION}</a>
- </div>
- <div class="${ns}-heading">Encode / Decode URL</div>
- <div class="${ns}-row">
- <input type="text" class="${ns}-decoded-input ${ns}-col" placeholder="https://">
- <input type="text" class="${ns}-encoded-input ${ns}-col" placeholder="https%3A%2F%2F">
- </div>
- `;
- settingsConfig.forEach(function addSetting(setting) {
- // Filter settings that aren't flagged to be displayed.
- if (!setting.showInSettings && !(setting.settings || []).find(s => s.showInSettings)) {
- return;
- }
- const desc = setting.description;
- tpl += `
- <div class="${ns}-row ${setting.isSubSetting ? `${ns}-sub-settings` : ''}">
- <div class="${ns}-col ${!setting.isSubSetting ? `${ns}-heading` : ''} ${desc ? `${ns}-has-description` : ''}" ${desc ? `title="${desc.replace(/"/g, '"')}"` : ''}>
- ${setting.title}
- ${(setting.actions || []).map(action => `<a href="javascript:;" class="${ns}-heading-action" data-handler="${action.handler}" data-property="${setting.property}">${action.title}</a>`)}
- </div>`;
- if (setting.settings) {
- setting.settings.forEach(subSetting => addSetting({
- ...setting,
- actions: null,
- settings: null,
- description: null,
- ...subSetting,
- isSubSetting: true
- }));
- } else {
- let value = _get(Player.config, setting.property, setting.default),
- attrs = (setting.attrs || '') + (setting.class ? ` class="${setting.class}"` : '') + ` data-property="${setting.property}"`;
- if (setting.format) {
- value = _get(Player, setting.format)(value);
- }
- let type = typeof value;
- if (setting.split) {
- value = value.join(setting.split);
- } else if (type === 'object') {
- value = JSON.stringify(value, null, 4);
- }
- tpl += `
- <div class="${ns}-col">
- ${
- type === 'boolean'
- ? `<input type="checkbox" ${attrs} ${value ? 'checked' : ''}></input>`
- : setting.showInSettings === 'textarea' || type === 'object'
- ? `<textarea ${attrs}>${value}</textarea>`
- : setting.options
- ? `<select ${attrs}>
- ${Object.keys(setting.options).map(k => `<option value="${k}" ${value === k ? 'selected' : ''}>
- ${setting.options[k]}
- </option>`).join('')}
- </select>`
- : `<input type="text" ${attrs} value="${value}"></input>`
- }
- </div>`;
- }
- tpl += '</div>';
- });
- return tpl;
- }
- }),
- /* 28 - Templates
- Thread browser */
- (function(module, exports) {
- module.exports = (data = {}) => `<div class="${ns}-heading ${ns}-has-description" title="Search for threads with a sound OP">
- Active Threads
- ${!Player.threads.loading ? `- <a class="${ns}-fetch-threads-link ${ns}-heading-action" href="javascript:;">Update</a>` : ''}
- </div>
- <div style="display: ${Player.threads.loading ? 'block' : 'none'}">Loading</div>
- <div style="display: ${Player.threads.loading ? 'none' : 'block'}">
- <div class="${ns}-heading ${ns}-has-description" title="Only includes threads containing the search.">
- Filter
- </div>
- <input type="text" class="${ns}-threads-filter" value="${Player.threads.filterValue || ''}"></input>
- <div class="${ns}-heading">
- Boards - <a class="${ns}-all-boards-link ${ns}-heading-action" href="javascript:;">${Player.threads.showAllBoards ? 'Selected Only' : 'Show All'}</a>
- </div>
- <div class="${ns}-thread-board-list">
- ${Player.templates.threadBoards(data)}
- </div>
- ${
- !Player.threads.hasParser || Player.config.threadsViewStyle === 'table'
- ? `<table style="width: 100%">
- <tr>
- <th>Thread</th>
- <th>Subject</th>
- <th>Replies/Images</th>
- <th>Started</th>
- <th>Updated</th>
- <tr>
- <tbody class="${ns}-threads-body"></tbody>
- </table>`
- : `<div class="${ns}-thread-list"></div>`
- }
- </div>`
- }),
- /* 29 - Templates
- Thread browser */
- (function(module, exports) {
- module.exports = (data = {}) => (Player.threads.boardList || []).map(board => {
- let checked = Player.threads.selectedBoards.includes(board.board);
- return !checked && !Player.threads.showAllBoards ?
- '' :
- `<label>
- <input type="checkbox" value="${board.board}" ${checked ? 'checked' : ''}>
- /${board.board}/
- </label>`
- }).join('')
- }),
- /* 30 - Templates
- Thread browser */
- (function(module, exports) {
- module.exports = (data = {}) => Object.keys(Player.threads.displayThreads).reduce((rows, board) => {
- return rows.concat(Player.threads.displayThreads[board].map(thread => `
- <tr>
- <td>
- <a class="quotelink" href="//boards.${thread.ws_board ? '4channel' : '4chan'}.org/${thread.board}/thread/${thread.no}#p${thread.no}" target="_blank">
- >>>/${thread.board}/${thread.no}
- </a>
- </td>
- <td>${thread.sub || ''}</td>
- <td>${thread.replies} / ${thread.images}</td>
- <td>${timeAgo(thread.time)}</td>
- <td>${timeAgo(thread.last_modified)}</td>
- </tr>
- `))
- }, []).join('')
- })
- ]);