[GMT] Tags Helper

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

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

  1. // ==UserScript==
  2. // @name [GMT] Tags Helper
  3. // @version 1.01.3
  4. // @author Anakunda
  5. // @copyright 2021, Anakunda (https://greasyfork.org/users/321857-anakunda)
  6. // @license GPL-3.0-or-later
  7. // @namespace https://greasyfork.org/users/321857-anakunda
  8. // @run-at document-end
  9. // @match https://*/artist.php?id=*
  10. // @match https://*/artist.php?*&id=*
  11. // @match https://*/requests.php
  12. // @match https://*/requests.php?submit=true&*
  13. // @match https://*/requests.php?type=*
  14. // @match https://*/requests.php?page=*
  15. // @match https://*/requests.php?action=new*
  16. // @match https://*/requests.php?action=view&id=*
  17. // @match https://*/requests.php?action=view&*&id=*
  18. // @match https://*/requests.php?action=edit&id=*
  19. // @match https://*/torrents.php?id=*
  20. // @match https://*/torrents.php
  21. // @match https://*/torrents.php?action=advanced
  22. // @match https://*/torrents.php?action=advanced&*
  23. // @match https://*/torrents.php?*&action=advanced
  24. // @match https://*/torrents.php?*&action=advanced&*
  25. // @match https://*/torrents.php?action=basic
  26. // @match https://*/torrents.php?action=basic&*
  27. // @match https://*/torrents.php?*&action=basic
  28. // @match https://*/torrents.php?*&action=basic&*
  29. // @match https://*/torrents.php?page=*
  30. // @match https://*/torrents.php?action=notify
  31. // @match https://*/torrents.php?action=notify&*
  32. // @match https://*/torrents.php?type=*
  33. // @match https://*/collages.php?id=*
  34. // @match https://*/collages.php?page=*&id=*
  35. // @match https://*/collages.php?action=new
  36. // @match https://*/collages.php?action=edit&collageid=*
  37. // @match https://*/bookmarks.php?type=*
  38. // @match https://*/bookmarks.php?page=*
  39. // @match https://*/upload.php
  40. // @match https://*/upload.php&url=*
  41. // @match https://*/upload.php&tags=*
  42. // @match https://*/bookmarks.php?type=torrents
  43. // @match https://*/bookmarks.php?page=*&type=torrents
  44. // @match https://*/top10.php
  45. // @match https://*/top10.php?*
  46. // @grant GM_getValue
  47. // @grant GM_setClipboard
  48. // @grant GM_registerMenuCommand
  49. // @require https://openuserjs.org/src/libs/Anakunda/QobuzLib.min.js
  50. // @require https://openuserjs.org/src/libs/Anakunda/GazelleTagManager.min.js
  51. // @description Improvements for working with groups of tags + increased efficiency of new requests creation
  52. // ==/UserScript==
  53.  
  54. (function() {
  55. 'use strict';
  56.  
  57. const hasStyleSheet = styleSheet => document.styleSheetSets && document.styleSheetSets.contains(styleSheet);
  58. const isLightTheme = ['postmod', 'shiro', 'layer_cake', 'proton', 'red_light'].some(hasStyleSheet);
  59. const isDarkTheme = ['kuro', 'minimal', 'red_dark'].some(hasStyleSheet);
  60. const uriTest = /^(https?:\/\/.+)$/i;
  61. const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || Boolean(window.InstallTrigger);
  62. const fieldNames = ['tags', 'tagname', 'taglist'];
  63. const exclusions = GM_getValue('exclusions', [
  64. '/^(?:\\d{4}s)$/i',
  65. '/^(?:delete\.this\.tag)$/i',
  66. ]).map(function(expr) {
  67. const m = /^\/(.+)\/([dgimsuy]*)$/.exec(expr);
  68. if (m != null) return new RegExp(m[1], m[2]);
  69. }).filter(it => it instanceof RegExp);
  70.  
  71. const getTagsFromIterable = iterable => Array.from(iterable)
  72. .filter(elem => elem.offsetWidth > 0 && elem.offsetHeight > 0 && elem.pathname && elem.search
  73. && fieldNames.some(URLSearchParams.prototype.has.bind(new URLSearchParams(elem.search))))
  74. .map(elem => elem.textContent.trim())
  75. .filter(tag => /^([a-z\d\.]+)$/.test(tag) && !exclusions.some(rx => rx.test(tag)));
  76.  
  77. const contextId = 'cae67c72-9aa7-4b96-855e-73cb23f5c7f8';
  78. let menuHooks = 0, menuInvoker;
  79.  
  80. function createMenu() {
  81. const menu = document.createElement('menu');
  82. menu.type = 'context';
  83. menu.id = contextId;
  84. menu.className = 'tags-helper';
  85.  
  86. function addMenuItem(label, callback) {
  87. if (label) {
  88. let menuItem = document.createElement('MENUITEM');
  89. menuItem.label = label;
  90. if (typeof callback == 'function') menuItem.onclick = callback;
  91. menu.append(menuItem);
  92. }
  93. return menu.children.length;
  94. }
  95.  
  96. addMenuItem('Copy tags to clipboard', function(evt) {
  97. console.assert(menuInvoker instanceof HTMLElement, 'menuInvoker instanceof HTMLElement')
  98. if (!(menuInvoker instanceof HTMLElement)) throw 'Invalid invoker';
  99. const tags = getTagsFromIterable(menuInvoker.getElementsByTagName('A'));
  100. if (tags.length > 0) GM_setClipboard(tags.join(', '), 'text');
  101. });
  102. addMenuItem('Make new request using these tags', function(evt) {
  103. console.assert(menuInvoker instanceof HTMLElement, 'menuInvoker instanceof HTMLElement')
  104. if (!(menuInvoker instanceof HTMLElement)) throw 'Invalid invoker';
  105. const tags = getTagsFromIterable(menuInvoker.getElementsByTagName('A'));
  106. if (tags.length > 0) document.location.assign('/requests.php?' + new URLSearchParams({
  107. action: 'new',
  108. tags: JSON.stringify(tags),
  109. }).toString());
  110. });
  111. addMenuItem('Make new upload using these tags', function(evt) {
  112. console.assert(menuInvoker instanceof HTMLElement, 'menuInvoker instanceof HTMLElement')
  113. if (!(menuInvoker instanceof HTMLElement)) throw 'Invalid invoker';
  114. const tags = getTagsFromIterable(menu.getElementsByTagName('A'));
  115. if (tags.length > 0) document.location.assign('/upload.php?' + new URLSearchParams({
  116. tags: JSON.stringify(tags),
  117. }).toString());
  118. });
  119. document.body.append(menu);
  120. }
  121.  
  122. function setElemHandlers(elem) {
  123. console.assert(elem instanceof HTMLElement);
  124. elem.addEventListener('click', function(evt) {
  125. if (evt.altKey) evt.preventDefault(); else return;
  126. const tags = getTagsFromIterable(evt.currentTarget.getElementsByTagName('A'));
  127. if (tags.length > 0) if (evt.ctrlKey) document.location.assign('/requests.php?' + new URLSearchParams({
  128. action: 'new',
  129. tags: JSON.stringify(tags)
  130. }).toString()); else if (evt.shiftKey) document.location.assign('/upload.php?' + new URLSearchParams({
  131. tags: JSON.stringify(tags)
  132. }).toString()); else {
  133. GM_setClipboard(tags.join(', '), 'text');
  134. evt.currentTarget.style.backgroundColor = isDarkTheme ? 'darkgreen' : 'lightgreen';
  135. setTimeout(elem => { elem.style.backgroundColor = null }, 1000, evt.currentTarget);
  136. }
  137. return false;
  138. });
  139. elem.ondragover = evt => false;
  140. elem.ondragenter = evt => { evt.currentTarget.style.backgroundColor = 'lawngreen' };
  141. elem[isFirefox ? 'ondragexit' : 'ondragleave'] = evt => { evt.currentTarget.style.backgroundColor = null };
  142. elem.draggable = true;
  143. elem.ondragstart = function(evt) {
  144. //evt.dataTransfer.clearData('text/uri-list');
  145. //evt.dataTransfer.clearData('text/x-moz-url');
  146. evt.dataTransfer.setData('text/plain',
  147. getTagsFromIterable(evt.currentTarget.getElementsByTagName('A')).join(', '));
  148. console.debug(evt.currentTarget, evt.currentTarget.getElementsByTagName('A'),
  149. getTagsFromIterable(evt.currentTarget.getElementsByTagName('A')));
  150. };
  151. elem.ondrop = function(evt) {
  152. evt.preventDefault();
  153. evt.stopPropagation();
  154. let links = evt.dataTransfer.getData('text/uri-list');
  155. if (links) links = links.split(/\r?\n/); else {
  156. links = evt.dataTransfer.getData('text/x-moz-url');
  157. if (links) links = links.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
  158. else if (links = evt.dataTransfer.getData('text/plain'))
  159. links = links.split(/\r?\n/).filter(RegExp.prototype.test.bind(uriTest));
  160. }
  161. if (Array.isArray(links) && links.length > 0) {
  162. const tags = getTagsFromIterable(evt.currentTarget.getElementsByTagName('A'));
  163. if (tags.length > 0) if (evt.shiftKey) document.location.assign('/upload.php?' + new URLSearchParams({
  164. url: links[0],
  165. tags: JSON.stringify(tags),
  166. }).toString()); else document.location.assign('/requests.php?' + new URLSearchParams({
  167. action: 'new',
  168. url: links[0],
  169. tags: JSON.stringify(tags),
  170. }).toString());
  171. }
  172. evt.currentTarget.style.backgroundColor = null;
  173. return false;
  174. };
  175. elem.setAttribute('contextmenu', contextId);
  176. elem.oncontextmenu = evt => { menuInvoker = evt.currentTarget };
  177. elem.style.cursor = 'context-menu';
  178. ++menuHooks;
  179. elem.title = `Alt + click => copy tags to clipboard
  180. Ctrl + Alt + click => make new request using these tags
  181. Shift + Alt + click => make new upload using these tags
  182. ---
  183. Drag & drop active link here => make new request using these tags
  184. Shift + Drag & drop active link here => make new upload using these tags
  185. Drag this tags area and drop to any text input to get inserted all tags as comma-separated list
  186. --or-- use context menu (older browsers only)`;
  187. }
  188.  
  189. switch (document.location.pathname) {
  190. case '/torrents.php': {
  191. if (!new URLSearchParams(document.location.search).has('id')) break;
  192. const urlParams = new URLSearchParams(document.location.search);
  193. try {
  194. let tags = urlParams.get('tags');
  195. if (tags && (tags = JSON.parse(tags)).length > 0) {
  196. const input = document.getElementById('tagname');
  197. if (input == null) throw 'Tags input not found';
  198. tags = new TagManager(...tags);
  199. input.value = tags.toString();
  200. input.scrollIntoView({ behavior: 'smooth', block: 'start' });
  201. //if (input.nextElementSibling != null) input.nextElementSibling.click();
  202. }
  203. } catch(e) { }
  204. break;
  205. }
  206. case '/requests.php':
  207. case '/upload.php': {
  208. const urlParams = new URLSearchParams(document.location.search);
  209. try {
  210. let tags = urlParams.get('tags');
  211. if (tags && (tags = JSON.parse(tags)).length > 0) {
  212. const input = document.getElementById('tags');
  213. if (input == null) throw 'Tags input not found';
  214. tags = new TagManager(...tags);
  215. input.value = tags.toString();
  216. }
  217. } catch(e) { }
  218. const url = urlParams.get('url');
  219. if (uriTest.test(url)) {
  220. let ua = document.getElementById('ua-data');
  221. function feedData() {
  222. ua.value = url;
  223. if ((ua = document.getElementById('autofill-form-2')) == null) return; // assertion failed
  224. if (typeof ua.onclick == 'function') ua.onclick(); else ua.click();
  225. }
  226. if (ua != null) feedData(); else {
  227. const container = document.querySelector('form#request_form > table > tbody');
  228. if (container != null) {
  229. let counters = [0, 0], timeStamp = Date.now();
  230. const mo = new MutationObserver(function(mutationsList) {
  231. ++counters[0];
  232. for (let mutation of mutationsList) for (let node of mutation.addedNodes) {
  233. ++counters[1];
  234. if (node.nodeName != 'TR' || (ua = node.querySelector('textarea#ua-data')) == null) continue;
  235. console.log('Found UA data by trigger:', counters, (Date.now() - timeStamp) / 1000);
  236. clearTimeout(timer);
  237. return feedData();
  238. }
  239. }), timer = setTimeout(mo => { mo.disconnect() }, 10000, mo);
  240. mo.observe(container, { childList: true });
  241. }
  242. }
  243. }
  244. break;
  245. }
  246. }
  247.  
  248. document.body.querySelectorAll('div.tags').forEach(setElemHandlers);
  249.  
  250. (function() {
  251. const tagsBox = document.body.querySelector('div.box_tags');
  252. if (tagsBox != null) setElemHandlers(tagsBox); else return;
  253. const head = tagsBox.querySelector('div.head');
  254. if (head == null) return;
  255. const span = document.createElement('SPAN'), a = document.createElement('A');
  256. span.style.float = 'right';
  257. a.className = 'brackets';
  258. a.textContent = 'Copy';
  259. a.href = '#';
  260. a.onclick = function(evt) {
  261. let tags = getTagsFromIterable(tagsBox.querySelectorAll('* > li > a'));
  262. if (tags.length <= 0) return false;
  263. GM_setClipboard(tags.join(', '), 'text');
  264. evt.currentTarget.style.color = isDarkTheme ? 'darkgreen' : 'lightgreen';
  265. setTimeout(elem => {elem.style.color = null }, 1000, evt.currentTarget);
  266. return false;
  267. };
  268. span.append(a);
  269. head.append(span);
  270. })();
  271.  
  272. if (menuHooks > 0) createMenu();
  273.  
  274. function groupDropHandler(evt) {
  275. if (evt.dataTransfer.items.length <= 0) return false;
  276. let tags = evt.dataTransfer.getData('text/plain');
  277. if (tags) tags = tags.split(/[\r\n\,\;\|\>]+/).map(expr => expr.trim()).filter(Boolean); else return false;
  278. if (tags.length > 0) tags = new TagManager(...tags); else return false;
  279. if (tags.length <= 0) return false;
  280. const a = evt.currentTarget.querySelector('a[href^="torrents.php?id="]');
  281. if (a != null) document.location.assign(a.href + '&tags=' + encodeURIComponent(JSON.stringify(tags)));
  282. return false;
  283. }
  284.  
  285. for (let tr of document.body.querySelectorAll([
  286. 'div#discog_table > table > tbody > tr.group',
  287. 'table.torrent_table > tbody > tr.group',
  288. 'table.torrent_table > tbody > tr.torrent',
  289. ].join(', '))) {
  290. tr.ondragover = evt => false;
  291. tr.ondrop = groupDropHandler;
  292. }
  293.  
  294. function inputDataHandler(evt) {
  295. switch (evt.type) {
  296. case 'paste': var tags = evt.clipboardData; break;
  297. case 'drop': tags = evt.dataTransfer; break;
  298. }
  299. if (tags) tags = tags.getData('text/plain'); else return;
  300. if (tags) tags = tags.split(/[\r\n\,\;\|\>]+/).map(expr => expr.trim()).filter(Boolean); else return;
  301. if (tags.length > 0) switch (evt.type) {
  302. case 'paste': tags = new TagManager(...tags); break;
  303. case 'drop': tags = new TagManager(evt.currentTarget.value, ...tags); break;
  304. }
  305. if (tags.length > 0) tags = tags.toString(); else return;
  306. switch (evt.type) {
  307. case 'paste': {
  308. const cursor = evt.currentTarget.selectionStart + tags.length;
  309. evt.currentTarget.value = evt.currentTarget.value.slice(0, evt.currentTarget.selectionStart) +
  310. tags + evt.currentTarget.value.slice(evt.currentTarget.selectionEnd);
  311. evt.currentTarget.selectionEnd = evt.currentTarget.selectionStart = cursor;
  312. break;
  313. }
  314. case 'drop': evt.currentTarget.value = tags; break;
  315. }
  316. return false;
  317. }
  318.  
  319. for (let input of document.body.querySelectorAll(fieldNames.map(name => `input[name="${name}"]`).join(', ')))
  320. input.onpaste = input.ondrop = inputDataHandler;
  321. })();