[GMT] Tags Helper

Improvements for working with groups of tags + increased efficiency of new requests creation

当前为 2021-07-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         [GMT] Tags Helper
// @namespace    https://greasyfork.org/users/321857-anakunda
// @version      1.01.2
// @match        https://*/artist.php?id=*
// @match        https://*/artist.php?*&id=*
// @match        https://*/requests.php
// @match        https://*/requests.php?submit=true&*
// @match        https://*/requests.php?type=*
// @match        https://*/requests.php?page=*
// @match        https://*/requests.php?action=new*
// @match        https://*/requests.php?action=view&id=*
// @match        https://*/requests.php?action=view&*&id=*
// @match        https://*/requests.php?action=edit&id=*
// @match        https://*/torrents.php?id=*
// @match        https://*/torrents.php
// @match        https://*/torrents.php?action=advanced&*
// @match        https://*/torrents.php?*&action=advanced
// @match        https://*/torrents.php?*&action=advanced&*
// @match        https://*/torrents.php?action=basic&*
// @match        https://*/torrents.php?*&action=basic
// @match        https://*/torrents.php?*&action=basic&*
// @match        https://*/torrents.php?page=*
// @match        https://*/torrents.php?action=notify
// @match        https://*/torrents.php?action=notify&*
// @match        https://*/torrents.php?type=*
// @match        https://*/collages.php?id=*
// @match        https://*/collages.php?page=*&id=*
// @match        https://*/collages.php?action=new
// @match        https://*/collages.php?action=edit&collageid=*
// @match        https://*/bookmarks.php?type=*
// @match        https://*/bookmarks.php?page=*
// @match        https://*/upload.php
// @match        https://*/upload.php&url=*
// @match        https://*/upload.php&tags=*
// @match        https://*/bookmarks.php?type=torrents
// @match        https://*/bookmarks.php?page=*&type=torrents
// @match        https://*/top10.php
// @match        https://*/top10.php?*
// @run-at       document-end
// @author       Anakunda
// @copyright    2021, Anakunda (https://greasyfork.org/users/321857-anakunda)
// @license      GPL-3.0-or-later
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// @require      https://openuserjs.org/src/libs/Anakunda/QobuzLib.js
// @require      https://openuserjs.org/src/libs/Anakunda/GazelleTagManager.js
// @description Improvements for working with groups of tags + increased efficiency of new requests creation
// ==/UserScript==

(function() {
  'use strict';

	const hasStyleSheet = styleSheet => document.styleSheetSets && document.styleSheetSets.contains(styleSheet);
	const isLightTheme = ['postmod', 'shiro', 'layer_cake', 'proton', 'red_light'].some(hasStyleSheet);
	const isDarkTheme = ['kuro', 'minimal', 'red_dark'].some(hasStyleSheet);
	const uriTest = /^(https?:\/\/.+)$/i;
	const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || Boolean(window.InstallTrigger);
	const fieldNames = ['tags', 'tagname', 'taglist'];
	const exclusions = GM_getValue('exclusions', [
		'/^(?:\\d{4}s)$/i',
		'/^(?:delete\.this\.tag)$/i',
	]).map(function(expr) {
		const m = /^\/(.+)\/([dgimsuy]*)$/.exec(expr);
		if (m != null) return new RegExp(m[1], m[2]);
	}).filter(it => it instanceof RegExp);

	const getTagsFromIterable = iterable => Array.from(iterable)
		.filter(elem => elem.offsetWidth > 0 && elem.offsetHeight > 0 && elem.pathname && elem.search
			&& fieldNames.some(URLSearchParams.prototype.has.bind(new URLSearchParams(elem.search))))
		.map(elem => elem.textContent.trim())
		.filter(tag => /^([a-z\d\.]+)$/.test(tag) && !exclusions.some(rx => rx.test(tag)));

	const contextId = 'cae67c72-9aa7-4b96-855e-73cb23f5c7f8';
	let menuHooks = 0, menuInvoker;

	function createMenu() {
		const menu = document.createElement('menu');
		menu.type = 'context';
		menu.id = contextId;
		menu.className = 'tags-helper';

		function addMenuItem(label, callback) {
			if (label) {
				let menuItem = document.createElement('MENUITEM');
				menuItem.label = label;
				if (typeof callback == 'function') menuItem.onclick = callback;
				menu.append(menuItem);
			}
			return menu.children.length;
		}

		addMenuItem('Copy tags to clipboard', function(evt) {
			console.assert(menuInvoker instanceof HTMLElement, 'menuInvoker instanceof HTMLElement')
			if (!(menuInvoker instanceof HTMLElement)) throw 'Invalid invoker';
			const tags = getTagsFromIterable(menuInvoker.getElementsByTagName('A'));
			if (tags.length > 0) GM_setClipboard(tags.join(', '), 'text');
		});
		addMenuItem('Make new request using these tags', function(evt) {
			console.assert(menuInvoker instanceof HTMLElement, 'menuInvoker instanceof HTMLElement')
			if (!(menuInvoker instanceof HTMLElement)) throw 'Invalid invoker';
			const tags = getTagsFromIterable(menuInvoker.getElementsByTagName('A'));
			if (tags.length > 0) document.location.assign('/requests.php?' + new URLSearchParams({
				action: 'new',
				tags: JSON.stringify(tags),
			}).toString());
		});
		addMenuItem('Make new upload using these tags', function(evt) {
			console.assert(menuInvoker instanceof HTMLElement, 'menuInvoker instanceof HTMLElement')
			if (!(menuInvoker instanceof HTMLElement)) throw 'Invalid invoker';
			const tags = getTagsFromIterable(menu.getElementsByTagName('A'));
			if (tags.length > 0) document.location.assign('/upload.php?' + new URLSearchParams({
				tags: JSON.stringify(tags),
			}).toString());
		});
		document.body.append(menu);
	}

	function setElemHandlers(elem) {
		console.assert(elem instanceof HTMLElement);
		elem.addEventListener('click', function(evt) {
			if (evt.altKey) evt.preventDefault(); else return;
			const tags = getTagsFromIterable(evt.currentTarget.getElementsByTagName('A'));
			if (tags.length > 0) if (evt.ctrlKey) document.location.assign('/requests.php?' + new URLSearchParams({
				action: 'new',
				tags: JSON.stringify(tags)
			}).toString()); else if (evt.shiftKey) document.location.assign('/upload.php?' + new URLSearchParams({
				tags: JSON.stringify(tags)
			}).toString()); else {
				GM_setClipboard(tags.join(', '), 'text');
				evt.currentTarget.style.backgroundColor = isDarkTheme ? 'darkgreen' : 'lightgreen';
				setTimeout(elem => { elem.style.backgroundColor = null }, 1000, evt.currentTarget);
			}
			return false;
		});
		elem.ondragover = evt => false;
		elem.ondragenter = evt => { evt.currentTarget.style.backgroundColor = 'lawngreen' };
		elem[isFirefox ? 'ondragexit' : 'ondragleave'] = evt => { evt.currentTarget.style.backgroundColor = null };
		elem.draggable = true;
		elem.ondragstart = function(evt) {
			//evt.dataTransfer.clearData('text/uri-list');
			//evt.dataTransfer.clearData('text/x-moz-url');
			evt.dataTransfer.setData('text/plain',
				getTagsFromIterable(evt.currentTarget.getElementsByTagName('A')).join(', '));
			console.debug(evt.currentTarget, evt.currentTarget.getElementsByTagName('A'),
				getTagsFromIterable(evt.currentTarget.getElementsByTagName('A')));
		};
		elem.ondrop = function(evt) {
			evt.preventDefault();
			let links = evt.dataTransfer.getData('text/uri-list');
			if (links) links = links.split(/\r?\n/); else {
				links = evt.dataTransfer.getData('text/x-moz-url');
				if (links) links = links.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
					else if (links = evt.dataTransfer.getData('text/plain'))
						links = links.split(/\r?\n/).filter(RegExp.prototype.test.bind(uriTest));
			}
			if (Array.isArray(links) && links.length > 0) {
				const tags = getTagsFromIterable(evt.currentTarget.getElementsByTagName('A'));
				if (tags.length > 0) if (evt.shiftKey) document.location.assign('/upload.php?' + new URLSearchParams({
					url: links[0],
					tags: JSON.stringify(tags),
				}).toString()); else document.location.assign('/requests.php?' + new URLSearchParams({
					action: 'new',
					url: links[0],
					tags: JSON.stringify(tags),
				}).toString());
			}
			evt.currentTarget.style.backgroundColor = null;
			return false;
		};
		elem.oncontextmenu = evt => { menuInvoker = evt.currentTarget };
		elem.setAttribute('contextmenu', contextId);
		++menuHooks;
		elem.title = `Alt + click => copy tags to clipboard
Ctrl + Alt + click => make new request using these tags
Shift + Alt + click => make new upload using these tags
---
Drag & drop active link here => make new request using these tags
Shift + Drag & drop active link here => make new upload using these tags
Drag this tags area and drop to any text input to get inserted all tags as comma-separated list
--or-- use context menu (older browsers only)`;
	}

	switch (document.location.pathname) {
		case '/requests.php':
		case '/upload.php': {
			const urlParams = new URLSearchParams(document.location.search);
			try {
				let tags = urlParams.get('tags');
				if (tags && (tags = JSON.parse(tags)).length > 0) {
					const input = document.getElementById('tags');
					if (input != null) input.value = tags.join(', ');
				}
			} catch(e) { }
			const url = urlParams.get('url');
			if (uriTest.test(url)) {
				let ua = document.getElementById('ua-data');
				function feedData() {
					ua.value = url;
					if ((ua = document.getElementById('autofill-form-2')) == null) return; // assertion failed
					if (typeof ua.onclick == 'function') ua.onclick(); else ua.click();
				}
				if (ua != null) feedData(); else {
					const container = document.querySelector('form#request_form > table > tbody');
					if (container != null) {
						let counters = [0, 0], timeStamp = Date.now();
						const mo = new MutationObserver(function(mutationsList) {
							++counters[0];
							for (let mutation of mutationsList) for (let node of mutation.addedNodes) {
								++counters[1];
								if (node.nodeName != 'TR' || (ua = node.querySelector('textarea#ua-data')) == null) continue;
								console.log('Found UA data by trigger:', counters, (Date.now() - timeStamp) / 1000);
								clearTimeout(timer);
								return feedData();
							}
						}), timer = setTimeout(mo => { mo.disconnect() }, 10000, mo);
						mo.observe(container, { childList: true });
					}
				}
			}
			break;
		}
	}

	document.body.querySelectorAll('div.tags').forEach(setElemHandlers);

	(function() {
		const tagsBox = document.body.querySelector('div.box_tags');
		if (tagsBox != null) setElemHandlers(tagsBox); else return;
		const head = tagsBox.querySelector('div.head');
		if (head == null) return;
		const span = document.createElement('SPAN'), a = document.createElement('A');
		span.style.float = 'right';
		a.className = 'brackets';
		a.textContent = 'Copy';
		a.href = '#';
		a.onclick = function(evt) {
			let tags = getTagsFromIterable(tagsBox.querySelectorAll('* > li > a'));
			if (tags.length <= 0) return false;
			GM_setClipboard(tags.join(', '), 'text');
			evt.currentTarget.style.color = isDarkTheme ? 'darkgreen' : 'lightgreen';
			setTimeout(elem => {elem.style.color = null }, 1000, evt.currentTarget);
			return false;
		};
		span.append(a);
		head.append(span);
	})();

	if (menuHooks > 0) createMenu();

	function inputDataHandler(evt) {
		switch (evt.type) {
			case 'paste': var tags = evt.clipboardData.getData('text/plain'); break;
			case 'drop': tags = evt.dataTransfer.getData('text/plain'); break;
		}
		if (!tags) return;
		tags = new TagManager(...tags.split(/[\r\n\,\;\|\>]+/).map(expr => expr.trim()).filter(Boolean))
		if (tags.length > 0) tags = tags.toString(); else return;
		switch (evt.type) {
			case 'paste': {
				const cursor = evt.target.selectionStart + tags.length;
				evt.target.value = evt.target.value.slice(0, evt.target.selectionStart) +
					tags + evt.target.value.slice(evt.target.selectionEnd);
				evt.target.selectionEnd = evt.target.selectionStart = cursor;
				break;
			}
			case 'drop':
				evt.target.value = tags;
				break;
		}
		return false;
	}

	for (let input of document.body.querySelectorAll(fieldNames.map(name => `input[name="${name}"]`).join(', ')))
		input.onpaste = input.ondrop = inputDataHandler;
})();