[GMT] Collage Extensions

Direct browsing from torrent pages; quick groups removal, custom quick Add To Collage form

  1. // ==UserScript==
  2. // @name [GMT] Collage Extensions
  3. // @version 1.25.0
  4. // @description Direct browsing from torrent pages; quick groups removal, custom quick Add To Collage form
  5. // @author Anakunda
  6. // @license GPL-3.0-or-later
  7. // @copyright 2020, Anakunda (https://openuserjs.org/users/Anakunda)
  8. // @namespace https://greasyfork.org/users/321857-anakunda
  9. // @match https://*/collages.php?id=*
  10. // @match https://*/collages.php?page=*&id=*
  11. // @match https://*/collage.php?id=*
  12. // @match https://*/collage.php?page=*&id=*
  13. // @match https://*/artist.php?*id=*
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @grant GM_registerMenuCommand
  17. // @grant GM_openInTab
  18. // @grant GM_setClipboard
  19. // @require https://openuserjs.org/src/libs/Anakunda/libLocks.min.js
  20. // @require https://openuserjs.org/src/libs/Anakunda/gazelleApiLib.min.js
  21. // @require https://openuserjs.org/src/libs/Anakunda/libCtxtMenu.min.js
  22. // ==/UserScript==
  23.  
  24. 'use strict';
  25.  
  26. let userAuth = document.body.querySelector('input[name="auth"][value]');
  27. if (userAuth != null) userAuth = userAuth.value;
  28. else if ((userAuth = document.body.querySelector('li#nav_logout > a')) == null
  29. || !(userAuth = new URLSearchParams(userAuth.search).get('auth'))) throw 'Auth not found';
  30. let userId = document.body.querySelector('li#nav_userinfo > a.username');
  31. if (userId != null) {
  32. userId = new URLSearchParams(userId.search);
  33. userId = parseInt(userId.get('id'));
  34. }
  35.  
  36. function addToTorrentCollage(collageId, groupId) {
  37. if (!(collageId > 0)) throw 'collage id invalid';
  38. if (!(groupId > 0)) throw 'torrent group id invalid';
  39. return (ajaxApiKey ? queryAjaxAPI('addtocollage', { collageid: collageId }, { groupids: groupId })
  40. .then(response => response.groupsadded.includes(groupId) ? true : Promise.reject('Rejected'))
  41. : Promise.reject('API key not set')).catch(reason => queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then(collage =>
  42. !collage.torrentgroups.map(torrentgroup => parseInt(torrentgroup.id)).includes(groupId) ? collage.id
  43. : Promise.reject('already in collage')).then(collageId => new Promise(function(resolve, reject) {
  44. const xhr = new XMLHttpRequest, formData = new URLSearchParams({
  45. action: 'add_torrent',
  46. collageid: collageId,
  47. groupid: groupId,
  48. url: document.location.origin.concat('/torrents.php?id=', groupId),
  49. auth: userAuth,
  50. });
  51. xhr.open('POST', '/collages.php', true);
  52. xhr.onreadystatechange = function() {
  53. if (xhr.readyState < XMLHttpRequest.DONE) return;
  54. if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.readyState); else reject(defaultErrorHandler(xhr));
  55. };
  56. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  57. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  58. xhr.send(formData);
  59. })).then(readyState => queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then(collage =>
  60. collage.torrentgroups.map(torrentgroup => parseInt(torrentgroup.id)).includes(groupId)
  61. || Promise.reject('Error: not added for unknown reason'))));
  62. }
  63.  
  64. function removeFromTorrentCollage(collageId, groupId, question) {
  65. if (!confirm(question)) return Promise.reject('Cancelled');
  66. return new Promise(function(resolve, reject) {
  67. let xhr = new XMLHttpRequest, formData = new URLSearchParams({
  68. action: 'manage_handle',
  69. collageid: collageId,
  70. groupid: groupId,
  71. auth: userAuth,
  72. submit: 'Remove',
  73. });
  74. xhr.open('POST', '/collages.php', true);
  75. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  76. xhr.onreadystatechange = function() {
  77. if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
  78. if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.status); else reject(defaultErrorHandler(xhr));
  79. xhr.abort();
  80. };
  81. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  82. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  83. xhr.send(formData);
  84. });
  85. }
  86.  
  87. function addToArtistCollage(collageId, artistId) {
  88. if (!(collageId > 0)) throw 'collage id invalid';
  89. if (!(artistId > 0)) throw 'artist id invalid';
  90. return queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then(collage =>
  91. !collage.artists.map(artist => parseInt(artist.id)).includes(artistId) ? collage.id
  92. : Promise.reject('already in collage')).then(collageId => new Promise(function(resolve, reject) {
  93. const xhr = new XMLHttpRequest, formData = new URLSearchParams({
  94. action: 'add_artist',
  95. collageid: collageId,
  96. artistid: artistId,
  97. url: document.location.origin.concat('/artist.php?id=', artistId),
  98. auth: userAuth,
  99. });
  100. xhr.open('POST', '/collages.php', true);
  101. xhr.onreadystatechange = function() {
  102. if (xhr.readyState < XMLHttpRequest.DONE) return;
  103. if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.readyState); else reject(defaultErrorHandler(xhr));
  104. };
  105. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  106. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  107. xhr.send(formData);
  108. })).then(readyState => queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then(collage =>
  109. collage.artists.map(artist => parseInt(artist.id)).includes(artistId)
  110. || Promise.reject('Error: not added for unknown reason')));
  111. }
  112.  
  113. function removeFromArtistCollage(collageId, artistId, question) {
  114. if (!confirm(question)) return Promise.reject('Cancelled');
  115. return new Promise(function(resolve, reject) {
  116. let xhr = new XMLHttpRequest, formData = new URLSearchParams({
  117. action: 'manage_artists_handle',
  118. collageid: collageId,
  119. artistid: artistId,
  120. auth: userAuth,
  121. submit: 'Remove',
  122. });
  123. xhr.open('POST', '/collages.php', true);
  124. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  125. xhr.onreadystatechange = function() {
  126. if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
  127. if (xhr.status >= 200 && xhr.status < 400) resolve(xhr.status); else reject(defaultErrorHandler(xhr));
  128. xhr.abort();
  129. };
  130. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  131. xhr.ontimeout = function() { reject(defaultTimeoutHandler(xhr)) };
  132. xhr.send(formData);
  133. });
  134. }
  135.  
  136. function addQuickAddForm() {
  137. if (!userId || !groupId && !artistId) return; // User id missing
  138. let ref = document.querySelector('div.sidebar');
  139. if (ref == null) return; // Sidebar missing
  140. const addSuccess = 'Successfully added to collage.';
  141. const alreadyInCollage = 'Error: This ' +
  142. (groupId ? 'torrent group' : artistId ? 'artist' : null) + ' is already in this collage';
  143. new Promise(function(resolve, reject) {
  144. try {
  145. var categories = JSON.parse(GM_getValue(document.location.hostname + '-categories'));
  146. if (categories.length > 0) resolve(categories); else throw 'empty list cached';
  147. } catch(e) {
  148. let xhr = new XMLHttpRequest;
  149. xhr.open('GET', '/collages.php', true);
  150. xhr.responseType = 'document';
  151. xhr.onload = function() {
  152. if (xhr.status >= 200 && xhr.status < 400) {
  153. categories = [ ];
  154. xhr.response.querySelectorAll('tr#categories > td > label').forEach(function(label, index) {
  155. let input = xhr.response.querySelector('tr#categories > td > input#' + label.htmlFor);
  156. categories[input != null && /\[(\d+)\]/.test(input.name) ? parseInt(RegExp.$1) : index] = label.textContent.trim();
  157. });
  158. if (categories.length > 0) {
  159. GM_setValue(document.location.hostname + '-categories', JSON.stringify(categories));
  160. resolve(categories);
  161. } else reject('Site categories could not be extracted');
  162. } else reject(defaultErrorHandler(xhr));
  163. };
  164. xhr.onerror = function() { reject(defaultErrorHandler(xhr)) };
  165. xhr.ontimeout = function() { reject(defaultTimeoutHandler()) };
  166. xhr.send();
  167. }
  168. }).then(function(categories) {
  169. const artistsIndexes = categories
  170. .map((category, index) => /^(?:Artists)$/i.test(category) ? index : -1)
  171. .filter(index => index >= 0);
  172. if (artistId && artistsIndexes.length <= 0) throw 'Artists index not found';
  173. const isCompatibleCategory = categoryId => categoryId >= 0 && categoryId < categories.length
  174. && (groupId && !artistsIndexes.includes(categoryId) || artistId && artistsIndexes.includes(categoryId));
  175. document.head.appendChild(document.createElement('style')).innerHTML = `
  176. form#addtocollage optgroup { background-color: slategray; color: white; }
  177. form#addtocollage option { background-color: white; color: black; max-width: 290pt; }
  178. div.box_addtocollage > form { padding: 0px 10px; }
  179. `;
  180. let elem = document.createElement('div');
  181. elem.className = 'box box_addtocollage';
  182. elem.style = 'padding: 0 0 10px;';
  183. elem.innerHTML = `
  184. <div class="head" style="margin-bottom: 5px;"><strong>Add to Collage</strong></div>
  185. <div id="ajax_message" class="hidden center" style="padding: 7px 0px;"></div>
  186. <form id="searchcollages">
  187. <input id="searchforcollage" placeholder="Collage search" type="text" style="max-width: 10em;">
  188. <input id="searchforcollagebutton" value="Search" type="submit" style="max-width: 4em;">
  189. </form>
  190. <form id="addtocollage" class="add_form" name="addtocollage">
  191. <select name="collageid" id="matchedcollages" class="add_to_collage_select" style="width: 96%;">
  192. <input id="opencollage-btn" value="Open collage" type="button">
  193. <input id="addtocollage-btn" value="Add to collage" type="button">
  194. </form>
  195. `;
  196. ref.append(elem);
  197. let ajaxMessage = document.getElementById('ajax_message');
  198. let srchForm = document.getElementById('searchcollages');
  199. if (srchForm == null) throw new Error('#searchcollages missing');
  200. let searchText = document.getElementById('searchforcollage');
  201. if (searchText == null) throw new Error('#searchforcollage missing');
  202. let dropDown = document.getElementById('matchedcollages');
  203. if (dropDown == null) throw new Error('#matchedcollages missing');
  204. let doOpen = document.getElementById('opencollage-btn');
  205. let doAdd = document.getElementById('addtocollage-btn');
  206. if (doAdd == null) throw new Error('#addtocollage-btn missing');
  207. let searchforcollagebutton = document.getElementById('searchforcollagebutton');
  208. if (searchforcollagebutton != null) searchforcollagebutton.disabled = searchText.value.length <= 0;
  209. srchForm.onsubmit = searchSubmit;
  210. searchText.ondrop = evt => dataHandler(evt.currentTarget, evt.dataTransfer);
  211. searchText.onpaste = evt => dataHandler(evt.currentTarget, evt.clipboardData);
  212. searchText.oninput = function(evt) {
  213. if (searchforcollagebutton != null) searchforcollagebutton.disabled = evt.currentTarget.value.length <= 0;
  214. };
  215. if (doOpen != null) doOpen.onclick = openCollage;
  216. doAdd.onclick = addToCollage;
  217. let initTimeCap = GM_getValue('max_preload_time', 0); // max time in ms to preload the dropdown
  218. if (initTimeCap > 0) findCollages({ userid: userId, contrib: 1 }, initTimeCap);
  219.  
  220. function clearList() {
  221. while (dropDown.childElementCount > 0) dropDown.removeChild(dropDown.firstElementChild);
  222. }
  223.  
  224. function findCollages(query, maxSearchTime) {
  225. return typeof query == 'object' ? new Promise(function(resolve, reject) {
  226. let start = Date.now();
  227. searchFormEnable(false);
  228. clearList();
  229. elem = document.createElement('option');
  230. elem.text = 'Searching...';
  231. dropDown.add(elem);
  232. dropDown.selectedIndex = 0;
  233. let retryCount = 0, options = [ ];
  234. searchInternal();
  235.  
  236. function searchInternal(page) {
  237. if (maxSearchTime > 0 && Date.now() - start > maxSearchTime) {
  238. reject('Time limit exceeded');
  239. return;
  240. }
  241. let xhr = new XMLHttpRequest, _query = new URLSearchParams(query);
  242. if (!page) page = 1;
  243. _query.set('page', page);
  244. xhr.open('GET', '/collages.php?' + _query, true);
  245. xhr.responseType = 'document';
  246. xhr.onload = function() {
  247. if (xhr.status < 200 || xhr.status >= 400) throw defaultErrorHandler(xhr);
  248. xhr.response.querySelectorAll('table.collage_table > tbody > tr[class^="row"]').forEach(function(tr, rowNdx) {
  249. if ((ref = tr.querySelector(':scope > td:nth-of-type(1) > a')) == null) {
  250. console.warn('Page parsing error');
  251. return;
  252. }
  253. elem = document.createElement('option');
  254. if ((elem.category = categories.findIndex(category => category.toLowerCase() == ref.textContent.toLowerCase())) < 0
  255. && /\b(?:cats)\[(\d+)\]/i.test(ref.search)) elem.category = parseInt(RegExp.$1); // unsafe due to site bug
  256. if ((ref = tr.querySelector(':scope > td:nth-of-type(2) > a')) == null || !/\b(?:id)=(\d+)\b/i.test(ref.search)) {
  257. console.warn(`Unknown collage id (${xhr.responseURL}/${rowNdx})`);
  258. return;
  259. }
  260. elem.value = elem.collageId = parseInt(RegExp.$1);
  261. elem.text = elem.title = ref.textContent.trim();
  262. if ((ref = tr.querySelector(':scope > td:nth-of-type(3)')) != null) elem.size = parseInt(ref.textContent);
  263. if ((ref = tr.querySelector(':scope > td:nth-of-type(4)')) != null) elem.subscribers = parseInt(ref.textContent);
  264. if ((ref = tr.querySelector(':scope > td:nth-of-type(6) > a')) != null
  265. && /\b(?:id)=(\d+)\b/i.test(ref.search)) elem.author = parseInt(RegExp.$1);
  266. if (isCompatibleCategory(elem.category) && (elem.category != 0 || elem.author == userId)) options.push(elem);
  267. });
  268. if (xhr.response.querySelector('div.linkbox > a.pager_next') != null) searchInternal(page + 1); else {
  269. if (!Object.keys(query).includes('order'))
  270. options.sort((a, b) => (b.size || 0) - (a.size || 0)/*a.title.localeCompare(b.title)*/);
  271. resolve(options);
  272. }
  273. };
  274. xhr.onerror = function() {
  275. if (xhr.status == 0 && retryCount++ <= 10) setTimeout(function() { searchInternal(page) }, 200);
  276. else reject(defaultErrorHandler(xhr));
  277. };
  278. xhr.ontimeout = function() { reject(defaultTimeoutHandler()) };
  279. xhr.send();
  280. }
  281. }).then(function(options) {
  282. clearList();
  283. categories.forEach(function(category, ndx) {
  284. let _category = options.filter(option => option.category == ndx);
  285. if (_category.length <= 0) return;
  286. elem = document.createElement('optgroup');
  287. elem.label = category;
  288. elem.append(..._category);
  289. dropDown.add(elem);
  290. });
  291. dropDown.selectedIndex = 0;
  292. searchFormEnable(true);
  293. return options;
  294. }).catch(function(reason) {
  295. clearList();
  296. searchFormEnable(true);
  297. console.warn(reason);
  298. }) : Promise.reject('Invalid parameter');
  299. }
  300.  
  301. function searchFormEnable(enabled) {
  302. for (let i = 0; i < srchForm.length; ++i) srchForm[i].disabled = !enabled;
  303. }
  304.  
  305. function searchSubmit(evt) {
  306. let searchTerm = searchText.value.trim();
  307. if (searchTerm.length <= 0) return false;
  308. let query = {
  309. action: 'search',
  310. search: searchTerm,
  311. type: 'c.name',
  312. order: 'Updated',
  313. sort: 'desc',
  314. order_way: 'Descending',
  315. };
  316. categories.map((category, index) => 'cats[' + index + ']')
  317. .filter((category, index) => isCompatibleCategory(index))
  318. .forEach(index => { query[index] = 1 });
  319. findCollages(query);
  320. return false;
  321. }
  322.  
  323. function addToCollage(evt) {
  324. (function() {
  325. evt.currentTarget.disabled = true;
  326. if (ajaxMessage != null) ajaxMessage.classList.add('hidden');
  327. let collageId = parseInt(dropDown.value);
  328. if (!collageId) return Promise.reject('No collage selected');
  329. /*
  330. if (Array.from(document.querySelectorAll('table.collage_table > tbody > tr:not([class="colhead"]) > td > a'))
  331. .map(node => /\b(?:id)=(\d+)\b/i.test(node.search) && parseInt(RegExp.$1)).includes(collageId))
  332. return Promise.reject(alreadyInCollage);
  333. */
  334. if (groupId > 0) return addToTorrentCollage(collageId, groupId);
  335. if (artistId > 0) return addToArtistCollage(collageId, artistId);
  336. return Promise.reject('munknown page class');
  337. })().then(function(collage) {
  338. if (ajaxMessage != null) {
  339. ajaxMessage.innerHTML = '<span style="color: #0A0;">' + addSuccess + '</span>';
  340. ajaxMessage.classList.remove('hidden');
  341. }
  342. evt.currentTarget.disabled = false;
  343. let mainColumn = document.querySelector('div.main_column');
  344. if (mainColumn == null) return collage;
  345. let tableName = collage.collageCategoryID != 0 ? 'collages' : 'personal_collages'
  346. let tbody = mainColumn.querySelector('table#' + tableName + ' > tbody');
  347. if (tbody == null) {
  348. tbody = document.createElement('tbody');
  349. tbody.innerHTML = '<tr class="colhead"><td width="85%"><a href="#">↑</a>&nbsp;</td><td># torrents</td></tr>';
  350. elem = document.createElement('table');
  351. elem.id = tableName;
  352. elem.className = 'collage_table';
  353. elem.append(tbody);
  354. mainColumn.insertBefore(elem, [
  355. 'table#personal_collages', 'table#vote_matches', 'div.torrent_description',
  356. 'div#similar_artist_map', 'div#artist_information',
  357. ].reduce((acc, selector) => acc || document.querySelector(selector), null));
  358. }
  359. tableName = '\xA0This ' + (artistsIndexes.includes(collage.collageCategoryID) ? 'artist' : 'album') + ' is in ' +
  360. tbody.childElementCount + ' ' + (collage.collageCategoryID != 0 ? 'collage' : 'personal collage');
  361. if (tbody.childElementCount > 1) tableName += 's';
  362. tbody.firstElementChild.firstElementChild.childNodes[1].data = tableName;
  363. elem = document.createElement('tr');
  364. elem.className = 'collage_rows';
  365. if (tbody.querySelector('tr.collage_rows.hidden') != null) elem.classList.add('hidden');
  366. elem.innerHTML = '<td><a href="/collages.php?id=' + collage.id + '">' + collage.name + '</a></td><td class="number_column">' +
  367. collage[artistsIndexes.includes(collage.collageCategoryID) ? 'artists' : 'torrentgroups'].length + '</td>';
  368. tbody.append(elem);
  369. return collage;
  370. }).catch(function(reason) {
  371. evt.currentTarget.disabled = false;
  372. if (ajaxMessage == null) return;
  373. ajaxMessage.innerHTML = '<span style="color: #A00;">' + reason.toString() + '</span>';
  374. ajaxMessage.classList.remove('hidden');
  375. });
  376. }
  377.  
  378. function openCollage(evt) {
  379. const collageId = parseInt(dropDown.value);
  380. if (collageId <= 0) return false;
  381. let win = window.open('/collages.php?id=' + collageId, '_blank');
  382. win.focus();
  383. }
  384.  
  385. function dataHandler(target, data) {
  386. const text = data.getData('text/plain');
  387. if (!text) return false;
  388. if (searchforcollagebutton != null) searchforcollagebutton.disabled = false;
  389. target.value = text;
  390. srchForm.onsubmit();
  391. return false;
  392. }
  393. });
  394. }
  395.  
  396. const contextId = 'context-9b7e0e42-1e35-4518-ac5f-b6bb31cce23f';
  397. let menu = document.createElement('menu');
  398. menu.type = 'context';
  399. menu.id = contextId;
  400. function contextUpdater(evt) { menu = evt.currentTarget }
  401. menu.innerHTML = '<menuitem label="Remove from this collage" icon="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAADaElEQVQ4y0XTT0xcVRQG8O+ce+fNAOUN7QiFFoHSNq2SIDQ2QGfQ0hYtSatpuvFPjK0mxtDWRF24MG7UbVeasDGmiTrWgG2NjRlaUyAsirrQoCZVF5QZhsKUP/NmKMwM993jwlG/zfdtT3J+IGYCwPE3zye+f//d6UrmMAAwkUI5/+6qilD4q7Z90x9tjyQIYAIIAHjo1ZfjMn5dNm9+LdfeOjdZQeSCCEzETMQAUBl03C/6j0wmD0Vl7kCnfFgXiQNgFT/32ndnTp84lVnK+A+8rHTubW7piNT2Xfvp5xEDFABIheLwZ6dPjR4u+odWvKxvQ0Hpcd32RrFdvD1cXV9az8Ffz4sqFdT8fNr0P76/O372hYQDbHFEtnz+4vOJJ6C7M/fmTSjoKBYrOuAgEnDqqZI5HH/97LcDPe29CytLJmCtLpV8U1dVo6+M3R6DdnCirr4vOTFhKiortEBMrVJ6ODU3eSGVOkkAUKmU++WZ50aPdu7pvp/NmwAcbQzZ6mA12VUPmbFb4hAYzCbCrK+m01ODs8mni0BOMZEqiRS++eX3kQ5365PtO3c059dLVvuai+tFyf34A3SxwBxQ9iHF6mo6fXtwNjlQBHIMKCWAMBEbSOHqb3fie0NVsUdrd+za9MWaP+6wZBZJBQO2CsSXU+mJ88nUUwZYY4AtYDUAQAQgghWB2SiCfIGZT8OfS0E7DhiAIoEAsOXfkHIrJlIWZEMk4Uv9x248+9iBWHZ12dq//mSGWMUkmsDCsNFtNbsaoY6MetkRHyj8d0KI4H7S1zc68Ehbz8pazmBmRmFjw1bpAAUJJAxRAi5AzMEat7mBAodveN6wDxRUiBAeisWun9zfFl1ZXzOcWdT+6rKJBIPqcjI9/msufze6raa1ADEk0EWI6drqNkVEx2553hX9aTQ6fnTPvo7lnGf0xgNtlpZMJODo4dT81Nup5DMA4LC6+VJTQ3ceviGBzlnfvNJc3yuWxzmTzy+YnAdd3CB/6b4fUUoPp+9NXZi9e9yA1gyw9sbszPFLyYUpl5UWgh9gULZgsVjaXAAB/F7r7ngyGpPF7i75uKll0gFcACiLYwAIgNyLD7dOer09MnuwSwYjO+MAGGWS/EFdQ2KosWU6RPQPZ+B/zuUdJApfbNw9/U5dUwJlzn8DiOOFPhubkGUAAAAASUVORK5CYII=" /><menuitem label="-" />';
  402.  
  403. function subscribeCallback(evt) {
  404. let link = menu || evt.relatedTarget || document.activeElement;
  405. if (!(link instanceof HTMLAnchorElement)) return true;
  406. let collageId = parseInt(new URLSearchParams(link.search).get('id'));
  407. if (!collageId) {
  408. console.warn('Assertion failed: no collage id', link);
  409. throw 'no id';
  410. }
  411. let xhr = new XMLHttpRequest;
  412. xhr.open('GET', '/userhistory.php?' + new URLSearchParams({
  413. action: 'collage_subscribe',
  414. collageid: collageId,
  415. auth: userAuth,
  416. }), true);
  417. xhr.onreadystatechange = function() {
  418. if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
  419. if (xhr.status >= 200 && xhr.status < 400) { console.info('Subscribed to collage id', collageId) }
  420. else console.error(defaultErrorHandler(xhr));
  421. xhr.abort();
  422. };
  423. xhr.send();
  424. }
  425.  
  426. const maxOpenTabs = GM_getValue('max_open_tabs', 25), autoCloseTimeout = GM_getValue('tab_auto_close_timeout', 0);
  427. let openedTabs = [ ], lastOnQueue;
  428. function openTabLimited(endpoint, params, hash) {
  429. if (typeof GM_openInTab != 'function') return Promise.reject('Not supported');
  430. if (!endpoint) return Promise.reject('Invalid argument');
  431. const waitFreeSlot = () => (maxOpenTabs > 0 && openedTabs.length >= maxOpenTabs ?
  432. Promise.race(openedTabs.map(tabHandler => new Promise(function(resolve) {
  433. console.assert(!tabHandler.closed);
  434. if (!tabHandler.closed) tabHandler.resolver = resolve; else resolve(tabHandler);
  435. }))) : Promise.resolve(null)).then(function(tabHandler) {
  436. console.assert(openedTabs.length <= maxOpenTabs);
  437. const url = new URL(endpoint + '.php', document.location.origin);
  438. if (params) for (let param in params) url.searchParams.set(param, params[param]);
  439. if (hash) url.hash = hash;
  440. (tabHandler = GM_openInTab(url.href, true)).onclose = function() {
  441. console.assert(this.closed);
  442. if (this.autoCloseTimer >= 0) clearTimeout(this.autoCloseTimer);
  443. const index = openedTabs.indexOf(this);
  444. console.assert(index >= 0);
  445. if (index >= 0) openedTabs.splice(index, 1);
  446. else openedTabs = openedTabs.filter(opernGroup => !opernGroup.closed);
  447. if (typeof this.resolver == 'function') this.resolver(this);
  448. }.bind(tabHandler);
  449. if (autoCloseTimeout > 0) tabHandler.autoCloseTimer = setTimeout(tabHandler =>
  450. { if (!tabHandler.closed) tabHandler.close() }, autoCloseTimeout * 1000, tabHandler);
  451. openedTabs.push(tabHandler);
  452. return tabHandler;
  453. });
  454. return lastOnQueue = lastOnQueue instanceof Promise ? lastOnQueue.then(waitFreeSlot) : waitFreeSlot();
  455. }
  456.  
  457. const urlParams = new URLSearchParams(document.location.search);
  458. let groupId, artistId, collageId;
  459. switch (document.location.pathname) {
  460. case '/torrents.php': {
  461. function cacheCollage(collage) {
  462. collages[document.domain][collage.id] = {
  463. id: collage.id,
  464. name: collage.name,
  465. torrentgroups: collage.torrentgroups.map(torrentgroup => ({
  466. id: torrentgroup.id,
  467. musicInfo: torrentgroup.musicInfo ? {
  468. artists: Array.isArray(torrentgroup.musicInfo.artists) ?
  469. torrentgroup.musicInfo.artists.map(artist => ({ name: artist.name })) : undefined,
  470. } : undefined,
  471. name: torrentgroup.name,
  472. year: parseInt(torrentgroup.year) || undefined,
  473. })),
  474. };
  475. window.sessionStorage.setItem('collages', JSON.stringify(collages));
  476. }
  477.  
  478. if (!(groupId = parseInt(urlParams.get('id'))) > 0) break; // Unexpected URL format
  479. const searchforcollage = document.getElementById('searchforcollage'),
  480. submitButton = document.getElementById('searchforcollagebutton'),
  481. addToCollageSelect = document.body.querySelector('select.add_to_collage_select');
  482. var collages;
  483. if (searchforcollage != null) {
  484. if (submitButton != null) submitButton.disabled = searchforcollage.value.length <= 0;
  485. if (typeof SearchCollage == 'function') SearchCollage = () => {
  486. const searchTerm = $('#searchforcollage').val(),
  487. personalCollages = $('#personalcollages');
  488. ajax.get(`ajax.php?action=collages&search=${encodeURIComponent(searchTerm)}`, responseText => {
  489. const { response, status } = JSON.parse(responseText);
  490. if (status !== 'success') return;
  491. const categories = response.reduce((accumulator, item) => {
  492. const { collageCategoryName } = item;
  493. accumulator[collageCategoryName] = (accumulator[collageCategoryName] || []).concat(item);
  494. return accumulator;
  495. }, {});
  496. personalCollages.children().remove();
  497. Object.entries(categories).forEach(([category, collages]) => {
  498. console.log(collages);
  499. personalCollages.append(`
  500. <optgroup label="${category}">
  501. ${collages.reduce((accumulator, { id, name }) =>
  502. `${accumulator}<option value="${id}">${name}</option>`
  503. ,'')}
  504. </optgroup>
  505. `);
  506. });
  507. });
  508. };
  509.  
  510. if (addToCollageSelect != null) addToCollageSelect.selectedIndex = -1;
  511.  
  512. function inputHandler(evt, key) {
  513. const data = evt[key].getData('text/plain').trim();
  514. if (!data) return true;
  515. evt.currentTarget.value = data;
  516. if (submitButton != null) submitButton.disabled = false;
  517. SearchCollage();
  518. setTimeout(function() {
  519. if (addToCollageSelect != null && addToCollageSelect.options.length > 1) {
  520. // TODO: expand
  521. }
  522. }, 3000);
  523. return false;
  524. }
  525. searchforcollage.onpaste = evt => inputHandler(evt, 'clipboardData');
  526. searchforcollage.ondrop = evt => inputHandler(evt, 'dataTransfer');
  527. searchforcollage.oninput = function(evt) {
  528. if (submitButton != null) submitButton.disabled = evt.currentTarget.value.length <= 0;
  529. };
  530. searchforcollage.onkeypress = function(evt) {
  531. if (evt.key == 'Enter' && evt.currentTarget.value.length > 0) SearchCollage();
  532. };
  533. } else addQuickAddForm();
  534.  
  535. if ('collages' in window.sessionStorage) try { collages = JSON.parse(window.sessionStorage.getItem('collages')) }
  536. catch(e) { console.warn(e) }
  537. if (!collages) collages = { };
  538. if (!(document.domain in collages)) collages[document.domain] = { };
  539.  
  540. function callback(evt) {
  541. switch (evt.currentTarget.nodeName) {
  542. case 'A':
  543. if (evt.button != 0 || !evt.altKey) return true;
  544. var link = evt.currentTarget;
  545. break;
  546. case 'MENUITEM':
  547. link = menu || evt.relatedTarget || document.activeElement;
  548. break;
  549. }
  550. if (!(link instanceof HTMLAnchorElement)) return true;
  551. let collageId = parseInt(new URLSearchParams(link.search).get('id'));
  552. if (!collageId) {
  553. console.warn('Assertion failed: no collage id', link);
  554. throw 'no id';
  555. }
  556. return removeFromTorrentCollage(collageId, groupId,
  557. 'Are you sure to remove this group from collage "' + link.textContent.trim() + '"?').then(function(status) {
  558. const tr = link.parentNode.parentNode, table = tr.parentNode.parentNode;
  559. tr.remove();
  560. if (table.querySelectorAll('tbody > tr:not([class="colhead"])').length <= 0) table.remove();
  561. });
  562. }
  563. menu.children[0].onclick = callback;
  564. let subscribeCmd = document.createElement('menuitem');
  565. subscribeCmd.label = 'Subscribe to this collage - toggle (!)';
  566. subscribeCmd.title = 'Use with care - toggling command; on already subscribed collages performs unsubscribe';
  567. subscribeCmd.onclick = subscribeCallback;
  568. menu.insertBefore(subscribeCmd, menu.children[1]);
  569. document.body.append(menu);
  570.  
  571. document.querySelectorAll('table[id$="collages"] > tbody > tr > td > a').forEach(function(link) {
  572. if (!link.pathname.startsWith('/collages.php') || !/\b(?:id)=(\d+)\b/.test(link.search)) return;
  573. let collageId = parseInt(RegExp.$1), toggle, navLinks = [ ],
  574. numberColumn = link.parentNode.parentNode.querySelector('td.number_column');
  575. link.onclick = callback;
  576. link.oncontextmenu = contextUpdater;
  577. link.setAttribute('contextmenu', contextId);
  578. link.title = 'Use Alt + left click or context menu(FF) to remove from this collage';
  579.  
  580. if (numberColumn != null) {
  581. numberColumn.style.cursor = 'pointer';
  582. numberColumn.onclick = loadCollage;
  583. numberColumn.title = collages[document.domain][collageId] ? 'Refresh' : 'Load collage for direct browsing';
  584. }
  585. if (collages[document.domain][collageId]) {
  586. expandSection();
  587. addCollageLinks(collages[document.domain][collageId]);
  588. }
  589.  
  590. function addCollageLinks(collage) {
  591. var index = collage.torrentgroups.findIndex(torrentgroup => parseInt(torrentgroup.id) == groupId);
  592. if (index < 0) {
  593. console.warn('Assertion failed: torrent', groupId, 'not found in the collage', collage);
  594. return false;
  595. }
  596. link.style.color = 'white';
  597. link.parentNode.parentNode.style = 'color:white; background-color: darkgoldenrod;';
  598. var stats = document.createElement('span');
  599. stats.textContent = (index + 1) + '/' + collage.torrentgroups.length;
  600. stats.style = 'font-size: 8pt; color: antiquewhite; font-weight: 100; margin-left: 10px;';
  601. navLinks.push(stats);
  602. link.parentNode.append(stats);
  603. if (collage.torrentgroups[index - 1]) {
  604. var a = document.createElement('a');
  605. a.href = '/torrents.php?id=' + collage.torrentgroups[index - 1].id;
  606. //a.classList.add('brackets');
  607. a.textContent = '[\xA0<\xA0]';
  608. a.title = getTitle(index - 1);
  609. a.style = 'color: chartreuse; margin-right: 10px;';
  610. navLinks.push(a);
  611. link.parentNode.prepend(a);
  612. a = document.createElement('a');
  613. a.href = '/torrents.php?id=' + collage.torrentgroups[0].id;
  614. //a.classList.add('brackets');
  615. a.textContent = '[\xA0<<\xA0]';
  616. a.title = getTitle(0);
  617. a.style = 'color: chartreuse; margin-right: 5px;';
  618. navLinks.push(a);
  619. link.parentNode.prepend(a);
  620. }
  621. if (collage.torrentgroups[index + 1]) {
  622. a = document.createElement('a');
  623. a.href = '/torrents.php?id=' + collage.torrentgroups[index + 1].id;
  624. //a.classList.add('brackets');
  625. a.textContent = '[\xA0>\xA0]';
  626. a.title = getTitle(index + 1);
  627. a.style = 'color: chartreuse; margin-left: 10px;';
  628. navLinks.push(a);
  629. link.parentNode.append(a);
  630. a = document.createElement('a');
  631. a.href = '/torrents.php?id=' + collage.torrentgroups[collage.torrentgroups.length - 1].id;
  632. //a.classList.add('brackets');
  633. a.textContent = '[\xA0>>\xA0]';
  634. a.title = getTitle(collage.torrentgroups.length - 1);
  635. a.style = 'color: chartreuse; margin-left: 5px;';
  636. navLinks.push(a);
  637. link.parentNode.append(a);
  638. }
  639. return true;
  640.  
  641. function getTitle(index) {
  642. if (typeof index != 'number' || index < 0 || index >= collage.torrentgroups.length) return undefined;
  643. let title = collage.torrentgroups[index].musicInfo && Array.isArray(collage.torrentgroups[index].musicInfo.artists) ?
  644. collage.torrentgroups[index].musicInfo.artists.map(artist => artist.name).join(', ') + ' - ' : '';
  645. if (collage.torrentgroups[index].name) title += collage.torrentgroups[index].name;
  646. if (collage.torrentgroups[index].year) title += ' (' + collage.torrentgroups[index].year + ')';
  647. return title;
  648. }
  649. }
  650.  
  651. function expandSection() {
  652. if (toggle === undefined) toggle = link.parentNode.parentNode.parentNode.querySelector('td > a[href="#"][onclick]');
  653. if (toggle === null || toggle.dataset.expanded) return false;
  654. toggle.dataset.expanded = true;
  655. toggle.click();
  656. return true;
  657. }
  658.  
  659. function loadCollage(evt) {
  660. evt.currentTarget.disabled = true;
  661. navLinks.forEach(a => { a.remove() });
  662. navLinks = [];
  663. let span = document.createElement('span');
  664. span.textContent = '[\xA0loading...\xA0]';
  665. span.style = 'color: red; background-color: white; margin-left: 10px;';
  666. link.parentNode.append(span);
  667. queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then(function(collage) {
  668. span.remove();
  669. cacheCollage(collage);
  670. addCollageLinks(collage);
  671. evt.currentTarget.disabled = false;
  672. }, function(reason) {
  673. span.remove();
  674. evt.currentTarget.disabled = false;
  675. });
  676. return false;
  677. }
  678. });
  679.  
  680. // GM_registerMenuCommand('Add to "Broken covers... collage',
  681. // () => { addToTorrentCollage(31445, groupId).catch(alert) });
  682. break;
  683. }
  684. case '/artist.php': {
  685. function cacheCollage(collage) {
  686. collages[document.domain][collage.id] = {
  687. id: collage.id,
  688. name: collage.name,
  689. artists: collage.artists.map(artist => ({
  690. id: artist.id,
  691. name: artist.name,
  692. })),
  693. };
  694. window.sessionStorage.setItem('collages', JSON.stringify(collages));
  695. }
  696.  
  697. if (!((artistId = parseInt(urlParams.get('id'))) > 0)) break; // Unexpected URL format
  698. addQuickAddForm();
  699. if ('collages' in window.sessionStorage) try { collages = JSON.parse(window.sessionStorage.getItem('collages')) }
  700. catch(e) { console.warn(e) }
  701. if (!collages) collages = { };
  702. if (!(document.domain in collages)) collages[document.domain] = { };
  703.  
  704. function callback(evt) {
  705. switch (evt.currentTarget.nodeName) {
  706. case 'A':
  707. if (evt.button != 0 || !evt.altKey) return true;
  708. var link = evt.currentTarget;
  709. break;
  710. case 'MENUITEM':
  711. link = menu || evt.relatedTarget || document.activeElement;
  712. break;
  713. }
  714. if (!(link instanceof HTMLAnchorElement)) return true;
  715. let collageId = parseInt(new URLSearchParams(link.search).get('id'));
  716. if (!collageId) {
  717. console.warn('Assertion failed: no collage id', link);
  718. throw 'no id';
  719. }
  720. return removeFromArtistCollage(collageId, artistId,
  721. 'Are you sure to remove this artist from collage "' + link.textContent.trim() + '"?').then(function(status) {
  722. const tr = link.parentNode.parentNode, table = tr.parentNode.parentNode;
  723. tr.remove();
  724. if (table.querySelectorAll('tbody > tr:not([class="colhead"])').length <= 0) table.remove();
  725. });
  726. }
  727. menu.children[0].onclick = callback;
  728. let subscribeCmd = document.createElement('menuitem');
  729. subscribeCmd.label = 'Subscribe to this collage - toggle (!)';
  730. subscribeCmd.title = 'Use with care - toggling command; on already subscribed collages performs unsubscribe';
  731. subscribeCmd.onclick = subscribeCallback;
  732. menu.insertBefore(subscribeCmd, menu.children[1]);
  733. document.body.append(menu);
  734.  
  735. document.querySelectorAll('table[id$="collages"] > tbody > tr > td > a').forEach(function(link) {
  736. if (!link.pathname.startsWith('/collages.php') || !/\b(?:id)=(\d+)\b/.test(link.search)) return;
  737. let collageId = parseInt(RegExp.$1), toggle, navLinks = [],
  738. numberColumn = link.parentNode.parentNode.querySelector('td:last-of-type');
  739. link.onclick = callback;
  740. link.oncontextmenu = contextUpdater;
  741. link.setAttribute('contextmenu', contextId);
  742. link.title = 'Use Alt + left click or context menu(FF) to remove from this collage';
  743.  
  744. if (numberColumn != null) {
  745. numberColumn.style.cursor = 'pointer';
  746. numberColumn.onclick = loadCollage;
  747. numberColumn.title = collages[document.domain][collageId] ? 'Refresh' : 'Load collage for direct browsing';
  748. }
  749. if (collages[document.domain][collageId]) {
  750. expandSection();
  751. addCollageLinks(collages[document.domain][collageId]);
  752. }
  753.  
  754. function addCollageLinks(collage) {
  755. var index = collage.artists.findIndex(artist => artist.id == artistId);
  756. if (index < 0) {
  757. console.warn('Assertion failed: torrent', groupId, 'not found in the collage', collage);
  758. return false;
  759. }
  760. link.style.color = 'white';
  761. link.parentNode.parentNode.style = 'color:white; background-color: darkgoldenrod;';
  762. var stats = document.createElement('span');
  763. stats.textContent = `${index + 1} / ${collage.artists.length}`;
  764. stats.style = 'font-size: 8pt; color: antiquewhite; font-weight: 100; margin-left: 10px;';
  765. navLinks.push(stats);
  766. link.parentNode.append(stats);
  767. if (collage.artists[index - 1]) {
  768. var a = document.createElement('a');
  769. a.href = '/artist.php?id=' + collage.artists[index - 1].id;
  770. a.textContent = '[\xA0<\xA0]';
  771. a.title = getTitle(index - 1);
  772. a.style = 'color: chartreuse; margin-right: 10px;';
  773. navLinks.push(a);
  774. link.parentNode.prepend(a);
  775. a = document.createElement('a');
  776. a.href = '/artist.php?id=' + collage.artists[0].id;
  777. a.textContent = '[\xA0<<\xA0]';
  778. a.title = getTitle(0);
  779. a.style = 'color: chartreuse; margin-right: 5px;';
  780. navLinks.push(a);
  781. link.parentNode.prepend(a);
  782. }
  783. if (collage.artists[index + 1]) {
  784. a = document.createElement('a');
  785. a.href = '/artist.php?id=' + collage.artists[index + 1].id;
  786. a.textContent = '[\xA0>\xA0]';
  787. a.title = getTitle(index + 1);
  788. a.style = 'color: chartreuse; margin-left: 10px;';
  789. navLinks.push(a);
  790. link.parentNode.append(a);
  791. a = document.createElement('a');
  792. a.href = '/artist.php?id=' + collage.artists[collage.artists.length - 1].id;
  793. a.textContent = '[\xA0>>\xA0]';
  794. a.title = getTitle(collage.artists.length - 1);
  795. a.style = 'color: chartreuse; margin-left: 5px;';
  796. navLinks.push(a);
  797. link.parentNode.append(a);
  798. }
  799. return true;
  800.  
  801. function getTitle(index) {
  802. console.assert(index >= 0 && index < collage.artists.length, "index >= 0 && index < collage.artists.length");
  803. if (!(index >= 0 && index < collage.artists.length)) return undefined;
  804. return collage.artists[index] ? collage.artists[index].name : '';
  805. }
  806. }
  807.  
  808. function expandSection() {
  809. if (toggle === undefined) toggle = link.parentNode.parentNode.parentNode.querySelector('td > a[href="#"][onclick]');
  810. if (toggle === null || toggle.dataset.expanded) return false;
  811. toggle.dataset.expanded = true;
  812. toggle.click();
  813. return true;
  814. }
  815.  
  816. function loadCollage(evt) {
  817. evt.currentTarget.disabled = true;
  818. navLinks.forEach(a => { a.remove() });
  819. navLinks = [ ];
  820. let span = document.createElement('span');
  821. span.textContent = '[\xA0loading...\xA0]';
  822. span.style = 'color: red; background-color: white; margin-left: 10px;';
  823. link.parentNode.append(span);
  824. queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then(function(collage) {
  825. span.remove();
  826. cacheCollage(collage);
  827. addCollageLinks(collage);
  828. evt.currentTarget.disabled = false;
  829. }, function(reason) {
  830. span.remove();
  831. evt.currentTarget.disabled = false;
  832. });
  833. return false;
  834. }
  835. });
  836. break;
  837. }
  838. case '/collages.php':
  839. case '/collage.php': {
  840. function addNotifier(caption, timeout = 20 * 1000) {
  841. if (!caption) return;
  842. const notifier = document.createElement('DIV');
  843. notifier.style = 'position: fixed; z-index: 999; top: 20pt; right: 20pt; padding: 10pt; border: 2pt solid gray; background-color: darkslategrey; color: gold; font: 600 10pt "Segoe UI", sans-serif; cursor: pointer;';
  844. notifier.textContent = caption;
  845. notifier.onclick = function(evt) {
  846. const ststsBox = document.body.querySelector('div.sidebar > div.box_info');
  847. if (ststsBox != null) ststsBox.scrollIntoView({ behavior: 'smooth', block: 'start' });
  848. };
  849. document.body.append(notifier);
  850. if (timeout > 0) setTimeout(function(elem) {
  851. if (timeout >= 4000) {
  852. elem.style.transition = 'opacity 2s';
  853. elem.style.opacity = 0;
  854. }
  855. setTimeout(elem => { document.body.removeChild(elem) }, Math.min(timeout, 2000), elem);
  856. }, Math.max(timeout - 2000, 0), notifier);
  857. }
  858. function updateGalleryPager(li) {
  859. }
  860.  
  861. if (!((collageId = parseInt(urlParams.get('id'))) > 0)) break; // Collage id missing
  862. let collageSize = document.body.querySelector('div.box_info > ul.stats > li:first-of-type');
  863. if (collageSize != null && (collageSize = /\b(\d+(?:[\,]\d+)*)\b/.exec(collageSize.textContent)) != null)
  864. collageSize = parseInt(collageSize[1].replace(/\D/g, ''));
  865. let category = document.querySelector('div.box_category > div.pad > a'), selectors, callback;
  866. category = category != null ? category.textContent : undefined;
  867. console.assert(category, 'category != undefined');
  868. let watchDogs = GM_getValue('watched_collages');
  869. if (!watchDogs || typeof watchDogs != 'object') watchDogs = { };
  870. if (!(document.domain in watchDogs)) watchDogs[document.domain] = { };
  871. const getCollageItemIds = () => queryAjaxAPI('collage', { id: collageId, showonlygroups: 1 }).then(collage =>
  872. collage[collage.collageCategoryID == 7 ? 'artists' : 'torrentgroups'].map(item => parseInt(item.id)));
  873. const saveSnapshot = () => getCollageItemIds().then(function(ids) {
  874. watchDogs[document.domain][collageId] = ids;
  875. GM_setValue('watched_collages', watchDogs);
  876. return watchDogs[document.domain][collageId].length;
  877. });
  878. if (collageId in watchDogs[document.domain]) {
  879. if (collageSize > 0 && collageSize != watchDogs[document.domain][collageId].length
  880. || !GM_getValue('savvy_change_detection', true)) getCollageItemIds().then(function(ids) {
  881. function addDelta(ids, caption) {
  882. const li = document.createElement('LI'),
  883. spans = ['SPAN', 'SPAN', 'SPAN'].map(Document.prototype.createElement.bind(document));
  884. spans[0].style = 'color: red; font-weight: 900;';
  885. spans[0].textContent = ids.length;
  886. spans[1].style = 'color: cadetblue; cursor: pointer;';
  887. spans[1].textContent = 'view';
  888. spans[1].onclick = function(evt) {
  889. for (let id of ids) openTabLimited(category == 'Artists' ? 'artist' : 'torrents', { id: id }, 'content');
  890. };
  891. spans[2].style = 'color: cadetblue; cursor: pointer;';
  892. spans[2].textContent = 'copy';
  893. spans[2].onclick = evt => { GM_setClipboard(ids.map(function(id) {
  894. if (evt.ctrlKey) {
  895. const url = new URL(`${category == 'Artists' ? 'artist' : 'torrents'}.php`, document.location.origin);
  896. url.searchParams.set('id', id);
  897. return url.href;
  898. }
  899. return id.toString();
  900. }).join('\n') + '\n', 'text') };
  901. li.append(spans[0], ` ${category == 'Artists' ? 'artist' : 'group'}(s) ${caption} (`);
  902. li.append(spans[1], ' | ', spans[2], ')');
  903. count.append(li);
  904. }
  905.  
  906. const count = document.body.querySelector('div.box_info > ul.stats > li:first-of-type');
  907. const addedIds = ids.filter(id => !watchDogs[document.domain][collageId].includes(id));
  908. if (addedIds.length > 0) addDelta(addedIds, 'added');
  909. const removedIds = watchDogs[document.domain][collageId].filter(id => !ids.includes(id));
  910. if (removedIds.length > 0) addDelta(removedIds, 'removed');
  911. if (addedIds.length <= 0 && removedIds.length <= 0) return;
  912. saveSnapshot();
  913. addNotifier('This collage has changed since last visit');
  914. });
  915. }
  916. const linkBox = document.body.querySelector('div#content div.header > div.linkbox');
  917. if (linkBox != null) {
  918. const a = document.createElement('A');
  919. a.className = 'brackets';
  920. a.href = '#';
  921. a.textContent = (a.watched = collageId in watchDogs[document.domain]) ? 'Unwatch' : 'Watch';
  922. a.title = 'Watched collages will highlight additions/removals since previous view on each load time';
  923. a.onclick = function toggleWatchState(evt) {
  924. if (!evt.currentTarget.disabled) evt.currentTarget.disabled = true; else return false;
  925. const currentTarget = evt.currentTarget;
  926. if (currentTarget.watched) {
  927. if (collageId in watchDogs[document.domain]) {
  928. delete watchDogs[document.domain][collageId];
  929. GM_setValue('watched_collages', watchDogs);
  930. }
  931. currentTarget.watched = false;
  932. currentTarget.textContent = 'Watch';
  933. currentTarget.disabled = false;
  934. } else saveSnapshot().then(function(collageSize) {
  935. currentTarget.watched = true;
  936. currentTarget.textContent = 'Unwatch';
  937. currentTarget.disabled = false;
  938. });
  939. return false;
  940. };
  941. linkBox.append(' ', a);
  942. }
  943.  
  944. if (category != 'Artists') {
  945. selectors = [
  946. 'tr.group > td[colspan] > strong > a[href^="torrents.php?id="]',
  947. 'ul.collage_images > li > a[href^="torrents.php?id="]',
  948. ];
  949. callback = function(evt) {
  950. switch (evt.currentTarget.nodeName) {
  951. case 'A':
  952. if (evt.button != 0 || !evt.altKey) return true;
  953. var link = evt.currentTarget;
  954. break;
  955. case 'MENUITEM':
  956. link = menu || evt.relatedTarget || document.activeElement;
  957. break;
  958. }
  959. if (!(link instanceof HTMLAnchorElement)) return true;
  960. let groupId = parseInt(new URLSearchParams(link.search).get('id'));
  961. if (!groupId) {
  962. console.warn('Assertion failed: no id', link);
  963. throw 'no id';
  964. }
  965. removeFromTorrentCollage(collageId, groupId, 'Are you sure to remove selected group from this collage?').then(function(status) {
  966. document.querySelectorAll(selectors.join(', ')).forEach(function(a) {
  967. if (parseInt(new URLSearchParams(a.search).get('id')) == groupId) switch (a.parentNode.nodeName) {
  968. case 'STRONG': a.parentNode.parentNode.parentNode.remove(); break;
  969. case 'LI': a.parentNode.remove(); break;
  970. }
  971. });
  972. });
  973. };
  974. } else {
  975. selectors = [
  976. 'table#discog_table > tbody > tr > td > a[href^="artist.php?id="]',
  977. 'ul.collage_images > li > a[href^="artist.php?id="]',
  978. ];
  979. callback = function(evt) {
  980. switch (evt.currentTarget.nodeName) {
  981. case 'A':
  982. if (evt.button != 0 || !evt.altKey) return true;
  983. var link = evt.currentTarget;
  984. break;
  985. case 'MENUITEM':
  986. link = menu || evt.relatedTarget || document.activeElement;
  987. break;
  988. }
  989. if (!(link instanceof HTMLAnchorElement)) return true;
  990. let artistId = parseInt(new URLSearchParams(link.search).get('id'));
  991. if (!artistId) {
  992. console.warn('Assertion failed: no id', evt.currentTarget);
  993. throw 'no id';
  994. }
  995. removeFromArtistCollage(collageId, artistId, 'Are you sure to remove selected artist from this collage?').then(function(status) {
  996. document.querySelectorAll(selectors.join(', ')).forEach(function(a) {
  997. if (parseInt(new URLSearchParams(a.search).get('id')) == artistId) switch (a.parentNode.nodeName) {
  998. case 'TD': a.parentNode.parentNode.remove(); break;
  999. case 'LI': a.parentNode.remove(); break;
  1000. }
  1001. });
  1002. });
  1003. };
  1004. let artistLink = document.querySelector('form.add_form[name="artist"] input#artist');
  1005. if (artistLink != null) {
  1006. let ref = document.querySelector('form.add_form[name="artist"] > div.submit_div');
  1007. let searchBtn = document.createElement('input');
  1008. searchBtn.value = 'Look up';
  1009. searchBtn.type = 'button';
  1010. searchBtn.onclick = function(evt) {
  1011. let xhr = new XMLHttpRequest;
  1012. xhr.open('HEAD', '/artist.php?artistname=' + encodeURIComponent(artistLink.value.trim()), true);
  1013. xhr.onreadystatechange = function() {
  1014. if (xhr.readyState < XMLHttpRequest.HEADERS_RECEIVED) return;
  1015. artistLink.value = xhr.responseURL.includes('/artist.php?id=') ? xhr.responseURL : '';
  1016. };
  1017. xhr.send();
  1018. };
  1019. ref.append(searchBtn);
  1020. }
  1021. }
  1022. menu.children[0].onclick = callback;
  1023. document.body.append(menu);
  1024. function handlerInstaller(a) {
  1025. a.onclick = callback;
  1026. a.oncontextmenu = contextUpdater;
  1027. a.setAttribute('contextmenu', contextId);
  1028. }
  1029. document.querySelectorAll(selectors.join(', ')).forEach(handlerInstaller);
  1030. let coverart = document.getElementById('coverart');
  1031. if (coverart != null) new MutationObserver(function(ml, mo) {
  1032. for (let mutation of ml) for (let node of mutation.addedNodes) {
  1033. if (node.nodeName != 'UL' || !node.classList.contains('collage_images')) return;
  1034. node.querySelectorAll(':scope > li > a').forEach(handlerInstaller);
  1035. const linkBox = document.body.querySelectorAll('div.main_column > div.linkbox.pager');
  1036. if (linkBox != null && GM_getValue('rearrange_page_control', false)) updateGalleryPager(linkBox);
  1037. }
  1038. }).observe(coverart, { childList: true });
  1039. if (!GM_getValue('rearrange_page_control', false)) break;
  1040. for (let linkBox of document.body.querySelectorAll('div.main_column > div.linkbox')) {
  1041. const page = parseInt(urlParams.get('page')) || 1, numPages = Math.ceil(collageSize / 50);
  1042. if (numPages > 1) if (linkBox.classList.contains('pager')) updateGalleryPager(linkBox); else {
  1043. const strong = linkBox.querySelector(':scope > strong');
  1044. console.assert(strong != null);
  1045. if (strong == null) continue;
  1046. let pager = linkBox.querySelector('a.pager_prev');
  1047. if (pager != null) {
  1048. const divisor = pager.nextSibling;
  1049. divisor.remove();
  1050. strong.insertAdjacentElement('beforebegin', pager);
  1051. strong.insertAdjacentText('beforebegin', divisor.wholeText);
  1052. if ((pager = linkBox.querySelector('a:first-of-type')) != null && pager.textContent.includes('<< First'))
  1053. pager.insertAdjacentText('afterend', divisor.wholeText);
  1054. }
  1055. if ((pager = linkBox.querySelector('a.pager_next')) != null) {
  1056. const divisor = pager.previousSibling;
  1057. divisor.remove();
  1058. strong.insertAdjacentElement('afterend', pager);
  1059. strong.insertAdjacentText('afterend', divisor.wholeText);
  1060. if ((pager = linkBox.querySelector('a:last-of-type')) != null && pager.textContent.includes('Last >>'))
  1061. pager.insertAdjacentText('beforebegin', divisor.wholeText);
  1062. }
  1063. const a = Array.prototype.filter.call(linkBox.querySelectorAll(':scope > a:not([class])'),
  1064. a => /\b(\d+(?:[\,]\d+)*)\s*-\s*(\d+(?:[\,]\d+)*)\b/.test(a.textContent));
  1065. let pageLinks;
  1066. if ((pageLinks = a.filter(a => parseInt(new URLSearchParams(a.search).get('page')) < page)).length > 0) {
  1067. const step = (page - 2) / (pageLinks.length + 1);
  1068. pageLinks.forEach(function(pageLink, index) {
  1069. const a = new URL(pageLink), p = 1 + Math.round((index + 1) * step);
  1070. a.searchParams.set('page', p);
  1071. pageLink.setAttribute('href', a.pathname + a.search);
  1072. pageLink.firstElementChild.textContent = `${p * 50 - 49}-${p * 50}`;
  1073. });
  1074. }
  1075. if ((pageLinks = a.filter(a => parseInt(new URLSearchParams(a.search).get('page')) > page)).length > 0) {
  1076. const step = (numPages - 1 - page) / (pageLinks.length + 1);
  1077. pageLinks.forEach(function(pageLink, index) {
  1078. const a = new URL(pageLink), p = page + 1 + Math.round((index + 1) * step);
  1079. a.searchParams.set('page', p);
  1080. pageLink.setAttribute('href', a.pathname + a.search);
  1081. pageLink.firstElementChild.textContent = `${p * 50 - 49}-${p * 50}`;
  1082. });
  1083. }
  1084. }
  1085. }
  1086. break;
  1087. }
  1088. }