4chan sounds

Play that faggy music weeb boi

目前为 2020-05-05 提交的版本。查看 最新版本

// ==UserScript==
// @name         4chan sounds
// @version      0.1.3
// @namespace    rccom
// @description  Play that faggy music weeb boi
// @author       RCC
// @match        *://boards.4chan.org/*
// @match        *://boards.4channel.org/*
// @grant        GM.getValue
// @grant        GM.setValue
// @run-at       document-start
// ==/UserScript==

(function() {
	'use strict';

	let isChanX;

	const repeatOptions = {
		all: 'Repeat All',
		one: 'Repeat One',
		none: 'No Repeat'
	};

	const Player = {
		ns: 'fc-sounds',
		sounds: [],
		container: null,
		ui: {},
		settings: {
			shuffle: false,
			repeat: Object.keys(repeatOptions)[0],
			autoshow: true,
			colors: {
				background: '#d6daf0',
				border: '#b7c5d9',
				odd_row: '#d6daf0',
				even_row: '#b7c5d9',
				expander: '#808bbf',
				expander_hover: '#9aa6e1',
				playing: '#98bff7'
			},
			allow: [
				"4cdn.org",
				"catbox.moe",
				"dmca.gripe",
				"lewd.se",
				"pomf.cat",
				"zz.ht"
			]
		},

		_templates: {
			css: ({ ns, colors }) =>
				`#${ns}-container {
					position: fixed;
					background: ${colors.background};
					border: 1px solid ${colors.border};
					display: relative;
					min-height: 200px;
					min-width: 100px;
				}
				.${ns}-show-settings .${ns}-player {
					display: none;
				}
				.${ns}-setting {
					display: none;
				}
				.${ns}-settings {
					display: none;
					padding: .25rem;
				}
				.${ns}-show-settings .${ns}-settings {
					display: block;
				}
				.${ns}-settings .${ns}-setting-header {
					font-weight: 600;
					margin-top: 0.25rem;
				}
				.${ns}-settings textarea {
					border: solid 1px ${colors.border};
					min-width: 100%;
					min-height: 4rem;
					box-sizing: border-box;
				}
				.${ns}-title {
					cursor: grab;
					text-align: center;
					border-bottom: solid 1px ${colors.border};
					padding: .25rem 0;
				}
				html.fourchan-x .${ns}-title a {
					font-size: 0;
					visibility: hidden;
					margin: 0 0.15rem;
				}
				html.fourchan-x  .${ns}-title .fa-repeat.fa-repeat-one::after {
					content: '1';
					font-size: .5rem;
					visibility: visible;
					margin-left: -1px;
				}
				.${ns}-image-link {
					height: 128px;
					text-align: center;
					display: flex;
					justify-items: center;
					justify-content: center;
					border-bottom: solid 1px ${colors.border};
				}
				.${ns}-image-link .${ns}-video {
					display: none;
				}
				.${ns}-image-link.${ns}-show-video .${ns}-video {
					display: block;
				}
				.${ns}-image-link.${ns}-show-video .${ns}-image {
					display: none;
				}
				.${ns}-image, .${ns}-video {
					max-height: 125px;
				}
				.${ns}-audio {
					width: 100%;
				}
				.${ns}-list-container {
					overflow: scroll;
				}
				.${ns}-list {
					display: grid;
					list-style-type: none;
					padding: 0;
					margin: 0;
				}
				.${ns}-list-item {
					list-style-type: none;
					padding: 0.15rem 0.25rem;
					white-space: nowrap;
					cursor: pointer;
				}
				.${ns}-list-item.playing {
					background: ${colors.playing} !important;
				}
				.${ns}-list-item:nth-child(n) {
					background: ${colors.odd_row};
				}
				.${ns}-list-item:nth-child(2n) {
					background: ${colors.even_row};
				}
				.${ns}-expander {
					position: absolute;
					bottom: 0px;
					right: 0px;
					height: 12px;
					width: 12px;
					cursor: se-resize;
					background: linear-gradient(to bottom right, rgba(0,0,0,0), rgba(0,0,0,0) 50%, ${colors.expander} 55%, ${colors.expander} 100%)
				}
				.${ns}-expander:hover {
					background: linear-gradient(to bottom right, rgba(0,0,0,0), rgba(0,0,0,0) 50%, ${colors.expander_hover} 55%, ${colors.expander_hover} 100%)
				}`,
			body: ({ ns }) =>
				`<div id="${ns}-container" style="top: 100px; left: 100px; width: 350px; display: none;">
					<div class="${ns}-title">
						<span style="float: left; margin-left: 0.25rem;">
							<a class="${ns}-repeat-button fa fa-repeat" href="javascript;">Repeat</a>
							<a class="${ns}-shuffle-button fa fa-random" href="javascript;">Ordered</a>
						</span>
						4chan Sounds
						<span style="float: right; margin-right: 0.25rem;">
							<a class="${ns}-config-button fa fa-wrench" title="Settings" href="javascript;">Settings</a>
							<a class="${ns}-close-button fa fa-times" href="javascript;">X</a>
						</span>
					</div>
					<div class="${ns}-player">
						<a class="${ns}-image-link" target="_blank">
							<img class="${ns}-image"></img>
							<video class="${ns}-video"></video>
						</a>
						<audio class="${ns}-audio" controls="true"></audio>
						<div class="${ns}-list-container" style="height: 100px">
							<ul class="${ns}-list">
							</ul>
						</div>
						<div class="${ns}-expander"></div>
					</div>
					<div class="${ns}-settings">
					</div>
				</div>`,
			list: ({ ns }) =>
				Player.sounds.map(sound => `<li class="${ns}-list-item" data-id="${sound.id}">${sound.title}</li>`).join(''),
			settings: ({ ns, colors, allow, autoshow }) =>
				`<div class="${ns}-setting-header" title="Automatically show the player when the thread contains sounds.">Autoshow</div>
				<input type="checkbox" data-property="autoshow" ${autoshow ? 'checked' : ''}></input>
				<div class="${ns}-setting-header" title="Which domains sources are allowed to be loaded from.">Allow</div>
				<textarea data-property="allow" data-split="linebreak">${allow.join('\n')}</textarea>
				<div class="${ns}-setting-header">Background Color</div>
				<input type="text" data-property="colors.background" value="${colors.background}"></input>
				<div class="${ns}-setting-header">Border Color</div>
				<input type="text" data-property="colors.border" value="${colors.border}"></input>
				<div class="${ns}-setting-header">Odd Row Color</div>
				<input type="text" data-property="colors.odd_row" value="${colors.odd_row}"></input>
				<div class="${ns}-setting-header">Even Row Color</div>
				<input type="text" data-property="colors.even_row" value="${colors.even_row}"></input>
				<div class="${ns}-setting-header">Playing Row Color</div>
				<input type="text" data-property="colors.playing" value="${colors.playing}"></input>
				<div class="${ns}-setting-header">Expand Color</div>
				<input type="text" data-property="colors.expander" value="${colors.expander}"></input>
				<div class="${ns}-setting-header">Expand Hover Color</div>
				<input type="text" data-property="colors.expander_hover" value="${colors.expander_hover}"></input>`
		},

		initialize: async function () {
			await Player.loadSettings();
			Player.sounds = [ ];
			Player.playOrder = [ ];

			if (isChanX) {
				const shortcuts = document.getElementById('shortcuts');
				const showIcon = document.createElement('span');
				shortcuts.insertBefore(showIcon, document.getElementById('shortcut-settings'));

				const attrs = { id: 'shortcut-sounds', class: 'shortcut brackets-wrap', 'data-index': 0 };
				for (let attr in attrs) {
					showIcon.setAttribute(attr, attrs[attr]);
				}
				showIcon.innerHTML = '<a href="javascript:;" title="Sounds" class="fa fa-play-circle">Sounds</a>';
				showIcon.querySelector('a').addEventListener('click', Player.toggleDisplay);
			} else {
				document.querySelectorAll('#settingsWindowLink, #settingsWindowLinkBot').forEach(function (link) {
					const bracket = document.createTextNode('] [');
					const showLink = document.createElement('a');
					showLink.innerHTML = 'Sounds';
					showLink.href = 'javascript;';
					link.parentNode.insertBefore(showLink, link);
					link.parentNode.insertBefore(bracket, link);
					showLink.addEventListener('click', Player.toggleDisplay);
				});
			}

			Player.render();
		},

		_tplOptions: function () {
			return { ns: Player.ns, ...Player.settings };
		},

		render: async function () {
			if (Player.container) {
				document.body.removeChild(Player.container);
				document.head.removeChild(Player.stylesheet);
			}

			// Insert the stylesheet
			Player.stylesheet = document.createElement('style');
			Player.stylesheet.innerHTML = Player._templates.css(Player._tplOptions());
			document.head.appendChild(Player.stylesheet);

			// Create the main player
			const el = document.createElement('div');
			el.innerHTML = Player._templates.body(Player._tplOptions());
			Player.container = el.querySelector(`#${Player.ns}-container`);
			document.body.appendChild(Player.container);
			// Keep track of various elements
			Player.ui.title = Player.container.querySelector(`.${Player.ns}-title`);
			Player.ui.closeButton = Player.container.querySelector(`.${Player.ns}-close-button`);
			Player.ui.repeatButton = Player.container.querySelector(`.${Player.ns}-repeat-button`);
			Player.ui.shuffleButton = Player.container.querySelector(`.${Player.ns}-shuffle-button`);
			Player.ui.configButton = Player.container.querySelector(`.${Player.ns}-config-button`)
			Player.ui.imageLink = Player.container.querySelector(`.${Player.ns}-image-link`);
			Player.ui.image = Player.container.querySelector(`.${Player.ns}-image`);
			Player.ui.video = Player.container.querySelector(`.${Player.ns}-video`);
			Player.ui.listContainer =  Player.container.querySelector(`.${Player.ns}-list-container`);
			Player.ui.list =  Player.container.querySelector(`.${Player.ns}-list`);
			Player.ui.settingsContainer =  Player.container.querySelector(`.${Player.ns}-settings`);
			Player.ui.expander = Player.container.querySelector(`.${Player.ns}-expander`);
			Player.audio = Player.container.querySelector(`.${Player.ns}-audio`);

			// Render the other bits and make sure the buttons states are correct
			Player.renderList();
			Player.renderSettings();
			Player.updateRepeatButton();
			Player.updateShuffleButton();

			// Add the event listeners for selecting a song
			Player.ui.list.addEventListener('click', function (e) {
				const id = e.target.getAttribute('data-id');
				const sound = id && Player.sounds.find(function (sound) {
					return sound.id === '' + id;
				});
				sound && Player.play(sound);
			});

			// Add event listeners for the title buttons
			Player.ui.closeButton.addEventListener('click', Player.hide);
			Player.ui.configButton.addEventListener('click', Player.toggleSettings);
			Player.ui.shuffleButton.addEventListener('click', Player.toggleShuffle);
			Player.ui.repeatButton.addEventListener('click', Player.toggleRepeat);

			// Add event listeners for moving/resizing
			Player.ui.expander.addEventListener('mousedown', Player.initResize, false);
			Player.ui.title.addEventListener('mousedown', Player.initMove, false);

			// Add audio event listeners
			Player.audio.addEventListener('ended', Player.next);
			Player.audio.addEventListener('pause', () => Player.ui.video.pause());
			Player.audio.addEventListener('play', () => {
				Player.ui.video.currentTime = Player.audio.currentTime;
				Player.ui.video.play();
			});
			Player.audio.addEventListener('seeked', () => Player.ui.video.currentTime = Player.audio.currentTime);
		},

		renderList: function () {
			if (Player.ui.list) {
				Player.ui.list.innerHTML = Player._templates.list(Player._tplOptions());
			}
		},

		renderSettings: function () {
			if (Player.ui.settingsContainer) {
				Player.ui.settingsContainer.innerHTML = Player._templates.settings(Player._tplOptions());
				Player.ui.settingsContainer.querySelectorAll('input, textarea').forEach(function (input) {
					input.addEventListener('blur', Player.handleSettingChange);
				});
				Player.ui.settingsContainer.querySelectorAll('input[type=checkbox]').forEach(function (input) {
					input.addEventListener('change', Player.handleSettingChange);
				});
			}
		},

		hide: function (e) {
			e && e.preventDefault();
			Player.container.style.display = 'none';
		},

		show: async function (e) {
			e && e.preventDefault();
			if (!Player.container.style.display) {
				return;
			}
			Player.container.style.display = null;
			// Apply the last position/size
			const [ top, left ] = (await GM.getValue(Player.ns + '.position') || '').split(':');
			const [ width, height ] = (await GM.getValue(Player.ns + '.size') || '').split(':');
			+width && +height && Player.resizeTo(width, height);
			+top && +left && Player.moveTo(top, left);
		},

		toggleDisplay: function (e) {
			e && e.preventDefault();
			if (Player.container.style.display === 'none') {
				Player.show();
			} else {
				Player.hide();
			}
		},

		saveSettings: function () {
			return GM.setValue(Player.ns + '.settings', JSON.stringify(Player.settings));
		},

		loadSettings: async function () {
			let settings = await GM.getValue(Player.ns + '.settings');
			if (!settings) {
				return;
			}
			try {
				settings = JSON.parse(settings);
			} catch(e) {
				return;
			}
			function _mix (to, from) {
				for (let key in from) {
					if (from[key] && typeof from[key] === 'object' && !Array.isArray(from[key])) {
						to[key] || (to[key] = {});
						_mix(to[key], from[key]);
					} else {
						to[key] = from[key];
					}
				}
			}
			_mix(Player.settings, settings);
		},

		handleSettingChange: function (e) {
			const input = e.currentTarget;
			const property = input.getAttribute('data-property').split('.');
			const split = input.getAttribute('data-split');
			const currentValue = property.reduce((v, k) => v && v[k], Player.settings);
			let newValue = input.getAttribute('type') === 'checkbox'
				? input.checked
				: input.value;
			if (split) {
				newValue = newValue.split(split === 'linebreak' ? '\n' : split);
			}
			// Not the most stringent check but enough to avoid some spamming.
			if (currentValue !== newValue) {
				// Update the setting.
				const lastProp = property.pop();
				const setOn = property.reduce((obj, k) => obj && obj[k], Player.settings);
				setOn && (setOn[lastProp] = newValue);

				// Update the stylesheet reflect any changes.
				Player.stylesheet.innerHTML = Player._templates.css(Player._tplOptions());

				// Save the new settings.
				Player.saveSettings();
			}
		},

		toggleSettings: function (e) {
			e.preventDefault();
			if (Player.container.classList.contains(Player.ns + '-show-settings')) {
				Player.container.classList.remove(Player.ns + '-show-settings');
			} else {
				Player.container.classList.add(Player.ns + '-show-settings');
			}
		},
		
		toggleShuffle: function (e) {
			e.preventDefault();
			Player.settings.shuffle = !Player.settings.shuffle;
			Player.updateShuffleButton();

			// Update the play order.
			if (!Player.settings.shuffle) {
				Player.playOrder = [ ...Player.sounds ];
			} else {
				const playOrder = Player.playOrder;
				for (let i = playOrder.length - 1; i > 0; i--) {
					const j = Math.floor(Math.random() * (i + 1));
					[playOrder[i], playOrder[j]] = [playOrder[j], playOrder[i]];
				}
			}
			Player.saveSettings();
		},

		updateShuffleButton: function () {
			const action = Player.settings.shuffle ? 'remove' : 'add';
			Player.ui.shuffleButton.classList[action]('disabled');
			Player.ui.shuffleButton.innerHTML = Player.settings.shuffle ? 'Shuffle' : 'Ordered';
			Player.ui.shuffleButton.title = isChanX && Player.ui.shuffleButton.innerHTML;
		},
		
		toggleRepeat: function (e) {
			e.preventDefault();
			const options = Object.keys(repeatOptions);
			const current = options.indexOf(Player.settings.repeat);
			Player.settings.repeat = options[(current + 4) % 3];
			Player.updateRepeatButton();
			Player.saveSettings();
		},

		updateRepeatButton: function () {
			Player.ui.repeatButton.innerHTML = repeatOptions[Player.settings.repeat];
			Player.ui.repeatButton.title = isChanX && Player.ui.repeatButton.innerHTML;
			const disabled = Player.settings.repeat === 'none';
			const addOne = Player.settings.repeat === 'one';
			Player.ui.repeatButton.classList[disabled ? 'add' : 'remove']('disabled');
			Player.ui.repeatButton.classList[addOne ? 'add' : 'remove']('fa-repeat-one');
		},

		initResize: function initDrag(e) {
			disableUserSelect();
			Player._startX = e.clientX;
			Player._startY = e.clientY;
			Player._startWidth = parseInt(document.defaultView.getComputedStyle(Player.container).width, 10);
			Player._startHeight = parseInt(document.defaultView.getComputedStyle(Player.container).height, 10);
			document.documentElement.addEventListener('mousemove', Player.doResize, false);
			document.documentElement.addEventListener('mouseup', Player.stopResize, false);
		},

		doResize: function(e) {
			Player.resizeTo(Player._startWidth + e.clientX - Player._startX, Player._startHeight + e.clientY - Player._startY);
		},

		resizeTo: function (width, height) {
			Player.container.style.width = width + 'px';
			Player.ui.listContainer.style.height = Math.max(10, height - 194) + 'px';
		},

		stopResize: function(e) {
			const style = document.defaultView.getComputedStyle(Player.container);
			document.documentElement.removeEventListener('mousemove', Player.doResize, false);
			document.documentElement.removeEventListener('mouseup', Player.stopResize, false);
			enableUserSelect();
			GM.setValue(Player.ns + '.size', parseInt(style.width, 10) + ':' + parseInt(style.height, 10));
		},

		initMove: function (e) {
			disableUserSelect();
			Player.ui.title.style.cursor = 'grabbing';
			Player._offsetX = e.clientX - Player.container.offsetLeft;
			Player._offsetY = e.clientY - Player.container.offsetTop;
			document.documentElement.addEventListener('mousemove', Player.doMove, false);
			document.documentElement.addEventListener('mouseup', Player.stopMove, false);
		},

		doMove: function (e) {
			Player.moveTo(e.clientX - Player._offsetX, e.clientY - Player._offsetY);
		},

		moveTo: function (x, y) {
			const style = document.defaultView.getComputedStyle(Player.container);
			const maxX = document.documentElement.clientWidth - parseInt(style.width, 10);
			const maxY = document.documentElement.clientHeight - parseInt(style.height, 10);
			Player.container.style.left = Math.max(0, Math.min(x, maxX)) + 'px';
			Player.container.style.top = Math.max(0, Math.min(y, maxY)) + 'px';
		},

		stopMove: function (e) {
			document.documentElement.removeEventListener('mousemove', Player.doMove, false);
			document.documentElement.removeEventListener('mouseup', Player.stopMove, false);
			Player.ui.title.style.cursor = null;
			enableUserSelect();
			GM.setValue(Player.ns + '.position', parseInt(Player.container.style.left, 10) + ':' + parseInt(Player.container.style.top, 10));
		},

		showThumb: function (sound) {
			Player.ui.imageLink.classList.remove(Player.ns + '-show-video');
			Player.ui.image.src = sound.thumb;
			Player.ui.imageLink.href = sound.image;
		},

		showImage: function (sound) {
			Player.ui.imageLink.classList.remove(Player.ns + '-show-video');
			Player.ui.image.src = sound.image;
			Player.ui.imageLink.href = sound.image;
		},

		playVideo: function (sound) {
			Player.ui.imageLink.classList.add(Player.ns + '-show-video');
			Player.ui.video.src = sound.image;
			Player.ui.video.play();
		},

		add: function (title, id, src, thumb, image) {
			const sound = { title, src, id, thumb, image };
			Player.sounds.push(sound);

			// Add the sound to the play order at the end, or someone random for shuffled.
			const index = Player.settings.shuffle
				? Math.floor(Math.random() * Player.sounds.length - 1)
				: Player.sounds.length;
			Player.playOrder.splice(index, 0, sound);

			// Re-render the list
			Player.renderList();

			// If nothing else has been added yet show the image for this sound.
			if (Player.playOrder.length === 1) {
				// If we're on a thread with autoshow enabled then make sure the player is displayed
				if (/\/thread\//.test(location.href) && Player.settings.autoshow) {
					Player.show();
				}
				Player.showThumb(sound);
			}
		},

		play: function (sound) {
			if (sound) {
				if (Player.playing) {
					const currentItem = Player.ui.list.querySelector('.playing');
					currentItem && currentItem.classList.remove('playing');
				}
				const item = Player.ui.list.querySelector(`li[data-id="${sound.id}"]`);
				item && item.classList.add('playing');
				Player.playing = sound;
				Player.audio.src = sound.src;
				if (sound.image.endsWith('.webm')) {
					Player.playVideo(sound);
				} else {
					Player.showImage(sound);
				}
			}
			Player.audio.play();
		},

		pause: function () {
			Player.audio.pause();
		},

		next: function () {
			Player._movePlaying(1);
		},

		previous: function () {
			Player._movePlaying(-1);
		},

		_movePlaying: function (direction) {
			// If there's no sound fall out.
			if (!Player.playOrder.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.playOrder.indexOf(Player.playing);
			if (currentIndex === -1) {
				return Player.playSound(Player.playOrder[0]);
			}
			// Get the next index, either repeating the same, wrapping round to repeat all or just moving the index.
			const nextIndex = Player.settings.repeat === 'one'
				? currentIndex
				: Player.settings.repeat === 'all'
					? ((currentIndex + direction) + Player.playOrder.length) % Player.playOrder.length
					: currentIndex + direction;
			const nextSound = Player.playOrder[nextIndex];
			nextSound && Player.play(nextSound);
		}
	};

	async function doInit() {
		await Player.initialize();

		parseFiles(document.body);

		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) {
							parseFiles(node);
						}
					});
				}
			});
		});

		observer.observe(document.body, {
			childList: true,
			subtree: true
		});
	};

	document.addEventListener("DOMContentLoaded", function (event) {
		setTimeout(function () {
			if (!isChanX) {
				doInit();
			}
		}, 1);
	});

	document.addEventListener( "4chanXInitFinished", function (event) {
		if (document.documentElement.classList.contains("fourchan-x") && document.documentElement.classList.contains("sw-yotsuba")) {
			isChanX = true;
			doInit();
		}
	});

	function parseFiles (target) {
		target.querySelectorAll(".post").forEach(function (post) {
			if (post.parentElement.parentElement.id === "qp" || post.parentElement.classList.contains("noFile")) {
				return;
			}
			post.querySelectorAll(".file").forEach(function (file) {
				parseFile(file, post);
			});
		});
	};

	function parseFile(file, post) {
		if (!file.classList.contains("file")) {
			return;
		}

		const fileLink = isChanX
			? file.querySelector(".fileText .file-info > a")
			: file.querySelector(".fileText > a");

		if (!fileLink) {
			return;
		}

		if (!fileLink.href) {
			return;
		}

		let fileName = null;

		if (isChanX) {
			[
				file.querySelector(".fileText .file-info .fnfull"),
				file.querySelector(".fileText .file-info > a")
			].some(function (node) {
				return node && (fileName = node.textContent);
			});
		} else {
			[
				file.querySelector(".fileText"),
				file.querySelector(".fileText > a")
			].some(function (node) {
				return node && (fileName = node.title || node.tagName === "A" && node.textContent);
			});
		}

		if (!fileName) {
			return;
		}

		fileName = fileName.replace(/\-/, "/");

		const match = fileName.match(/^(.*)[\[\(\{](?:audio|sound)[ \=\:\|\$](.*?)[\]\)\}]/i);

		if (!match) {
			return;
		}

		const id = post.id.slice(1);
		const name = match[1] || id;
		const fileThumb = post.querySelector('.fileThumb');
		const fullSrc = fileThumb && fileThumb.href;
		const thumbSrc = fileThumb && fileThumb.querySelector('img').src;
		let link = match[2];

		if (link.includes("%")) {
			try {
				link = decodeURIComponent(link);
			} catch (error) {
				return;
			}
		}

		if (link.match(/^(https?\:)?\/\//) === null) {
			link = (location.protocol + "//" + link);
		}

		try {
			link = new URL(link);
		} catch (error) {
			return;
		}

		for (let item of Player.settings.allow) {
			if (link.hostname.toLowerCase() === item || link.hostname.toLowerCase().endsWith("." + item)) {
				return Player.add(name, id, link.href, thumbSrc, fullSrc);
			}
		}
	};

	function disableUserSelect () {
		document.body.style.userSelect = 'none';
		document.body.style.MozUserSelect = 'none';
	}

	function enableUserSelect () {
		document.body.style.userSelect = null;
		document.body.style.MozUserSelect = null;
	}
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址