[GMT] Tags Helper

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

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

  1. // ==UserScript==
  2. // @name [GMT] Tags Helper
  3. // @version 1.01.5
  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. // @grant GM_getValue
  50. // @grant GM_setValue
  51. // @grant GM_deleteValue
  52. // @require https://openuserjs.org/src/libs/Anakunda/libLocks.min.js
  53. // @require https://openuserjs.org/src/libs/Anakunda/gazelleApiLib.min.js
  54. // @require https://openuserjs.org/src/libs/Anakunda/QobuzLib.min.js
  55. // @require https://openuserjs.org/src/libs/Anakunda/GazelleTagManager.min.js
  56. // @description Improvements for working with groups of tags + increased efficiency of new requests creation
  57. // ==/UserScript==
  58.  
  59. (function() {
  60. 'use strict';
  61.  
  62. const hasStyleSheet = styleSheet => document.styleSheetSets && document.styleSheetSets.contains(styleSheet);
  63. const isLightTheme = ['postmod', 'shiro', 'layer_cake', 'proton', 'red_light'].some(hasStyleSheet);
  64. const isDarkTheme = ['kuro', 'minimal', 'red_dark'].some(hasStyleSheet);
  65. const uriTest = /^(https?:\/\/.+)$/i;
  66. const isFirefox = /\b(?:Firefox)\b/.test(navigator.userAgent) || Boolean(window.InstallTrigger);
  67. const fieldNames = ['tags', 'tagname', 'taglist'];
  68. const exclusions = GM_getValue('exclusions', [
  69. '/^(?:\\d{4}s)$/i',
  70. '/^(?:delete\.this\.tag)$/i',
  71. ]).map(function(expr) {
  72. const m = /^\/(.+)\/([dgimsuy]*)$/.exec(expr);
  73. if (m != null) return new RegExp(m[1], m[2]);
  74. }).filter(it => it instanceof RegExp);
  75.  
  76. const getTagsFromIterable = iterable => Array.from(iterable)
  77. .filter(elem => elem.offsetWidth > 0 && elem.offsetHeight > 0 && elem.pathname && elem.search
  78. && fieldNames.some(URLSearchParams.prototype.has.bind(new URLSearchParams(elem.search))))
  79. .map(elem => elem.textContent.trim())
  80. .filter(tag => /^([a-z\d\.]+)$/.test(tag) && !exclusions.some(rx => rx.test(tag)));
  81.  
  82. const contextId = 'cae67c72-9aa7-4b96-855e-73cb23f5c7f8';
  83. let menuHooks = 0, menuInvoker;
  84.  
  85. function createMenu() {
  86. const menu = document.createElement('menu');
  87. menu.type = 'context';
  88. menu.id = contextId;
  89. menu.className = 'tags-helper';
  90.  
  91. function addMenuItem(label, callback) {
  92. if (label) {
  93. let menuItem = document.createElement('MENUITEM');
  94. menuItem.label = label;
  95. if (typeof callback == 'function') menuItem.onclick = callback;
  96. menu.append(menuItem);
  97. }
  98. return menu.children.length;
  99. }
  100.  
  101. addMenuItem('Copy tags to clipboard', function(evt) {
  102. console.assert(menuInvoker instanceof HTMLElement, 'menuInvoker instanceof HTMLElement')
  103. if (!(menuInvoker instanceof HTMLElement)) throw 'Invalid invoker';
  104. const tags = getTagsFromIterable(menuInvoker.getElementsByTagName('A'));
  105. if (tags.length > 0) GM_setClipboard(tags.join(', '), 'text');
  106. });
  107. addMenuItem('Make new request using these tags', function(evt) {
  108. console.assert(menuInvoker instanceof HTMLElement, 'menuInvoker instanceof HTMLElement')
  109. if (!(menuInvoker instanceof HTMLElement)) throw 'Invalid invoker';
  110. const tags = getTagsFromIterable(menuInvoker.getElementsByTagName('A'));
  111. if (tags.length > 0) document.location.assign('/requests.php?' + new URLSearchParams({
  112. action: 'new',
  113. tags: JSON.stringify(tags),
  114. }).toString());
  115. });
  116. addMenuItem('Make new upload using these tags', function(evt) {
  117. console.assert(menuInvoker instanceof HTMLElement, 'menuInvoker instanceof HTMLElement')
  118. if (!(menuInvoker instanceof HTMLElement)) throw 'Invalid invoker';
  119. const tags = getTagsFromIterable(menuInvoker.getElementsByTagName('A'));
  120. if (tags.length > 0) document.location.assign('/upload.php?' + new URLSearchParams({
  121. tags: JSON.stringify(tags),
  122. }).toString());
  123. });
  124. document.body.append(menu);
  125. }
  126.  
  127. function setElemHandlers(elem, textCallback) {
  128. console.assert(elem instanceof HTMLElement);
  129. elem.addEventListener('click', function(evt) {
  130. if (evt.altKey) evt.preventDefault(); else return;
  131. const tags = getTagsFromIterable(evt.currentTarget.getElementsByTagName('A'));
  132. if (tags.length > 0) if (evt.ctrlKey) document.location.assign('/requests.php?' + new URLSearchParams({
  133. action: 'new',
  134. tags: JSON.stringify(tags)
  135. }).toString()); else if (evt.shiftKey) document.location.assign('/upload.php?' + new URLSearchParams({
  136. tags: JSON.stringify(tags)
  137. }).toString()); else {
  138. GM_setClipboard(tags.join(', '), 'text');
  139. evt.currentTarget.style.backgroundColor = isDarkTheme ? 'darkgreen' : 'lightgreen';
  140. setTimeout(elem => { elem.style.backgroundColor = null }, 1000, evt.currentTarget);
  141. }
  142. return false;
  143. });
  144. elem.ondragover = evt => false;
  145. elem.ondragenter = evt => { evt.currentTarget.style.backgroundColor = 'lawngreen' };
  146. elem[isFirefox ? 'ondragexit' : 'ondragleave'] = evt => { evt.currentTarget.style.backgroundColor = null };
  147. elem.draggable = true;
  148. elem.ondragstart = function(evt) {
  149. //evt.dataTransfer.clearData('text/uri-list');
  150. //evt.dataTransfer.clearData('text/x-moz-url');
  151. evt.dataTransfer.setData('text/plain',
  152. getTagsFromIterable(evt.currentTarget.getElementsByTagName('A')).join(', '));
  153. console.debug(evt.currentTarget, evt.currentTarget.getElementsByTagName('A'),
  154. getTagsFromIterable(evt.currentTarget.getElementsByTagName('A')));
  155. };
  156. elem.ondrop = function(evt) {
  157. evt.stopPropagation();
  158. let links = evt.dataTransfer.getData('text/uri-list');
  159. if (links) links = links.split(/\r?\n/); else {
  160. links = evt.dataTransfer.getData('text/x-moz-url');
  161. if (links) links = links.split(/\r?\n/).filter((item, ndx) => ndx % 2 == 0);
  162. else if (links = evt.dataTransfer.getData('text/plain'))
  163. links = links.split(/\r?\n/).filter(RegExp.prototype.test.bind(uriTest));
  164. }
  165. if (Array.isArray(links) && links.length > 0) {
  166. const tags = getTagsFromIterable(evt.currentTarget.getElementsByTagName('A'));
  167. if (tags.length > 0) if (evt.shiftKey) document.location.assign('/upload.php?' + new URLSearchParams({
  168. url: links[0],
  169. tags: JSON.stringify(tags),
  170. }).toString()); else document.location.assign('/requests.php?' + new URLSearchParams({
  171. action: 'new',
  172. url: links[0],
  173. tags: JSON.stringify(tags),
  174. }).toString());
  175. } else if (typeof textCallback == 'function' && (links = evt.dataTransfer.getData('text/plain'))
  176. //&& (links = links.split(/[\r\n\,\;\|\>]+/).map(expr => expr.trim()).filter(Boolean)).length > 0
  177. && (links = new TagManager(links)).length > 0) textCallback(evt, links);
  178. evt.currentTarget.style.backgroundColor = null;
  179. return false;
  180. };
  181. elem.setAttribute('contextmenu', contextId);
  182. elem.oncontextmenu = evt => { menuInvoker = evt.currentTarget };
  183. elem.style.cursor = 'context-menu';
  184. ++menuHooks;
  185. elem.title = `Alt + click => copy tags to clipboard
  186. Ctrl + Alt + click => make new request using these tags
  187. Shift + Alt + click => make new upload using these tags
  188. ---
  189. Drag & drop active link here => make new request using these tags
  190. Shift + Drag & drop active link here => make new upload using these tags
  191. Drag this tags area and drop to any text input to get inserted all tags as comma-separated list
  192. --or-- use context menu (older browsers only)`;
  193. }
  194.  
  195. switch (document.location.pathname) {
  196. case '/torrents.php': {
  197. if (!new URLSearchParams(document.location.search).has('id')) break;
  198. const urlParams = new URLSearchParams(document.location.search);
  199. try {
  200. let tags = urlParams.get('tags');
  201. if (tags && (tags = JSON.parse(tags)).length > 0) {
  202. const input = document.getElementById('tagname');
  203. if (input == null) throw 'Tags input not found';
  204. tags = new TagManager(...tags);
  205. input.value = tags.toString();
  206. input.scrollIntoView({ behavior: 'smooth', block: 'start' });
  207. //if (input.nextElementSibling != null) input.nextElementSibling.click();
  208. }
  209. } catch(e) { }
  210. break;
  211. }
  212. case '/requests.php':
  213. case '/upload.php': {
  214. const urlParams = new URLSearchParams(document.location.search);
  215. try {
  216. let tags = urlParams.get('tags');
  217. if (tags && (tags = JSON.parse(tags)).length > 0) {
  218. const input = document.getElementById('tags');
  219. if (input == null) throw 'Tags input not found';
  220. tags = new TagManager(...tags);
  221. input.value = tags.toString();
  222. }
  223. } catch(e) { }
  224. const url = urlParams.get('url');
  225. if (uriTest.test(url)) {
  226. let ua = document.getElementById('ua-data');
  227. function feedData() {
  228. ua.value = url;
  229. if ((ua = document.getElementById('autofill-form-2')) == null) return; // assertion failed
  230. if (typeof ua.onclick == 'function') ua.onclick(); else ua.click();
  231. }
  232. if (ua != null) feedData(); else {
  233. const container = document.querySelector('form#request_form > table > tbody');
  234. if (container != null) {
  235. let counters = [0, 0], timeStamp = Date.now();
  236. const mo = new MutationObserver(function(mutationsList) {
  237. ++counters[0];
  238. for (let mutation of mutationsList) for (let node of mutation.addedNodes) {
  239. ++counters[1];
  240. if (node.nodeName != 'TR' || (ua = node.querySelector('textarea#ua-data')) == null) continue;
  241. console.log('Found UA data by trigger:', counters, (Date.now() - timeStamp) / 1000);
  242. clearTimeout(timer);
  243. return feedData();
  244. }
  245. }), timer = setTimeout(mo => { mo.disconnect() }, 10000, mo);
  246. mo.observe(container, { childList: true });
  247. }
  248. }
  249. }
  250. break;
  251. }
  252. }
  253.  
  254. document.body.querySelectorAll('div.tags').forEach(div => { setElemHandlers(div, function(evt, tags) {
  255. const a = evt.currentTarget.parentNode.querySelector('a[href^="torrents.php?id="]');
  256. if (a == null) return false;
  257. if (evt.ctrlKey && ajaxApiKey) {
  258. const tagsElement = evt.currentTarget, groupId = parseInt(new URLSearchParams(a.search).get('id')) || undefined;
  259. if (groupId) queryAjaxAPI('addtag', { groupid: groupId }, { tagname: tags.toString() }).then(function(response) {
  260. console.log(response);
  261. if (!['added', 'voted'].some(key => response[key].length > 0)) return;
  262. queryAjaxAPI('torrentgroup', { id: groupId }).then(function(response) {
  263. if (!response.group.tags) {
  264. document.location.reload();
  265. return;
  266. }
  267. const urlParams = new URLSearchParams(tagsElement.childElementCount > 0 ? tagsElement.children[0].search : {
  268. action: 'advanced',
  269. searchsubmit: 1,
  270. });
  271. while (tagsElement.childNodes.length > 0) tagsElement.removeChild(tagsElement.childNodes[0]);
  272. for (let tag of response.group.tags) {
  273. if (tagsElement.childElementCount > 0) tagsElement.append(', ');
  274. const a = document.createElement('A');
  275. for (let param of fieldNames) if (urlParams.has(param)) urlParams.set(param, a.textContent = tag);
  276. a.setAttribute('href', 'torrents.php?' + urlParams.toString());
  277. tagsElement.append(a);
  278. }
  279. });
  280. });
  281. } else document.location.assign(a.href + '&tags=' + encodeURIComponent(JSON.stringify(tags)));
  282. }) });
  283.  
  284. (function() {
  285. const tagsBox = document.body.querySelector('div.box_tags');
  286. if (tagsBox != null) setElemHandlers(tagsBox, function(evt, tags) {
  287. function fallBack() {
  288. const input = document.getElementById('tagname');
  289. if (input == null) throw 'Tags input not found';
  290. input.value = tags.toString();
  291. input.scrollIntoView({ behavior: 'smooth', block: 'start' });
  292. //if (input.nextElementSibling != null) input.nextElementSibling.click();
  293. }
  294.  
  295. const groupId = document.location.pathname == '/torrents.php'
  296. && parseInt(new URLSearchParams(document.location.search).get('id')) || undefined;
  297. if (!groupId) return fallBack();
  298. if (ajaxApiKey) {
  299. const tagsElement = evt.currentTarget.querySelector('ul') || evt.currentTarget.querySelector('ol');
  300. if (tagsElement != null) queryAjaxAPI('addtag', { groupid: groupId }, { tagname: tags.toString() }).then(function(response) {
  301. console.log(response);
  302. if (['added', 'voted'].some(key => response[key].length > 0)) document.location.reload();
  303. // queryAjaxAPI('torrentgroup', { id: groupId }).then(function(response) {
  304. // if (!response.group.tags) {
  305. // document.location.reload();
  306. // return;
  307. // }
  308. // let a = tagsElement.querySelector('li > a');
  309. // const urlParams = new URLSearchParams(a != null ? a.search : undefined);
  310. // while (tagsElement.childNodes.length > 0) tagsElement.removeChild(tagsElement.childNodes[0]);
  311. // for (let tag of response.group.tags) {
  312. // urlParams.set('taglist', (a = document.createElement('A')).textContent = tag);
  313. // a.setAttribute('href', 'torrents.php?' + encodeURIComponent(urlParams.toString()));
  314. // const li = document.createElement('LI');
  315. // li.append(a);
  316. // tagsElement.append(li);
  317. // }
  318. // });
  319. }); else fallBack();
  320. } else fallBack();
  321. }); else return;
  322. const head = tagsBox.querySelector('div.head');
  323. if (head == null) return;
  324. const span = document.createElement('SPAN'), a = document.createElement('A');
  325. span.style.float = 'right';
  326. a.className = 'brackets';
  327. a.textContent = 'Copy';
  328. a.href = '#';
  329. a.onclick = function(evt) {
  330. let tags = getTagsFromIterable(tagsBox.querySelectorAll('* > li > a'));
  331. if (tags.length <= 0) return false;
  332. GM_setClipboard(tags.join(', '), 'text');
  333. evt.currentTarget.style.color = isDarkTheme ? 'darkgreen' : 'lightgreen';
  334. setTimeout(elem => {elem.style.color = null }, 1000, evt.currentTarget);
  335. return false;
  336. };
  337. span.append(a);
  338. head.append(span);
  339. })();
  340.  
  341. if (menuHooks > 0) createMenu();
  342.  
  343. function inputDataHandler(evt) {
  344. switch (evt.type) {
  345. case 'paste': var tags = evt.clipboardData; break;
  346. case 'drop': tags = evt.dataTransfer; break;
  347. }
  348. if (tags) tags = tags.getData('text/plain'); else return;
  349. //if (tags) tags = tags.split(/[\r\n\;\|\>]+|,(?:\s*&)?/).map(expr => expr.trim()).filter(Boolean); else return;
  350. if (tags.length > 0) switch (evt.type) {
  351. case 'paste': tags = new TagManager(tags); break;
  352. case 'drop': tags = new TagManager(evt.currentTarget.value, tags); break;
  353. } else return;
  354. if (tags.length > 0) tags = tags.toString(); else return;
  355. evt.stopPropagation();
  356. switch (evt.type) {
  357. case 'paste': {
  358. const cursor = evt.currentTarget.selectionStart + tags.length;
  359. evt.currentTarget.value = evt.currentTarget.value.slice(0, evt.currentTarget.selectionStart) +
  360. tags + evt.currentTarget.value.slice(evt.currentTarget.selectionEnd);
  361. evt.currentTarget.selectionEnd = evt.currentTarget.selectionStart = cursor;
  362. break;
  363. }
  364. case 'drop': evt.currentTarget.value = tags; break;
  365. }
  366. return false;
  367. }
  368.  
  369. for (let input of document.body.querySelectorAll(fieldNames.map(name => `input[name="${name}"]`).join(', ')))
  370. input.onpaste = input.ondrop = inputDataHandler;
  371. })();