Greasyfork 快捷编辑收藏

在GF脚本页添加快速打开收藏集编辑页面功能

As of 2023-12-07. See the latest version.

  1. /* eslint-disable no-multi-spaces */
  2.  
  3. // ==UserScript==
  4. // @name Greasyfork 快捷编辑收藏
  5. // @name:zh-CN Greasyfork 快捷编辑收藏
  6. // @name:zh-TW Greasyfork 快捷編輯收藏
  7. // @name:en Greasyfork script-set-edit button
  8. // @name:en-US Greasyfork script-set-edit button
  9. // @name:fr Greasyfork Set Edit+
  10. // @namespace Greasyfork-Favorite
  11. // @version 0.2.2
  12. // @description 在GF脚本页添加快速打开收藏集编辑页面功能
  13. // @description:zh-CN 在GF脚本页添加快速打开收藏集编辑页面功能
  14. // @description:zh-TW 在GF腳本頁添加快速打開收藏集編輯頁面功能
  15. // @description:en Add / Remove script into / from script set directly in GF script info page
  16. // @description:en-US Add / Remove script into / from script set directly in GF script info page
  17. // @description:fr Ajouter un script à un jeu de scripts / supprimer un script d'un jeu de scripts directement sur la page d'informations sur les scripts GF
  18. // @author PY-DNG
  19. // @license GPL-3
  20. // @match http*://*.greatest.deepsurf.us/*
  21. // @match http*://*.sleazyfork.org/*
  22. // @match http*://greatest.deepsurf.us/*
  23. // @match http*://sleazyfork.org/*
  24. // @require https://greatest.deepsurf.us/scripts/456034-basic-functions-for-userscripts/code/script.js?version=1226884
  25. // @require https://greatest.deepsurf.us/scripts/460385-gm-web-hooks/code/script.js?version=1221394
  26. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAbBJREFUOE+Vk7GKGlEUhr8pAiKKDlqpCDpLUCzWBxCENBa+hBsL9wHsLWxXG4tNtcGH0MIiWopY7JSGEUWsbESwUDMw4Z7siLsZDbnlPff/7n/+e67G38sA6sAXIPVWXgA/gCdgfinRPuhfCoXCw3Q65XA4eLBl6zvw1S2eAZqmvTqOc5/NZhkMBqRSKWzbvgYxgbwquoAX4MGyLHK5HIlEgtFo9C+IOFEAo1gsWsvlUmyPx2MymYxAhsMh6XT6lpM7BXjWdf1xNpuRz+fl8GQywTAMGo0G1WpVnJxOJ692vinADPgcDAaZz+cCOR6PmKZJPB4XUb/fp1wuewF+KoBCf1JVBVE5dDodms3mWdDtdqlUKl6AX+8ALmS9XgtM0/5kvNlspKX9fv8RIgBp4bISCoXo9XqsVitKpRK6rrPb7STQ7XZ7eVRaeAYerz14OBxGOfL7/eIgmUwKzHEcJZEQ1eha1wBqPxqNihufzyeQWCzmtiPPqJYM0jWIyiISibBYLAgEAtTrdVqt1nmQXN0rcH/LicqmVqvRbrdN27bfjbKru+nk7ZD3Z7q4+b++82/YPKIrXsKZ3AAAAABJRU5ErkJggg==
  27. // @grant GM_xmlhttpRequest
  28. // @grant GM_setValue
  29. // @grant GM_getValue
  30. // ==/UserScript==
  31.  
  32. /* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */
  33. /* global GMXHRHook GMDLHook */
  34.  
  35. (function __MAIN__() {
  36. 'use strict';
  37.  
  38. const CONST = {
  39. Text: {
  40. 'zh-CN': {
  41. FavEdit: '收藏集:',
  42. Add: '加入此集',
  43. Remove: '移出此集',
  44. Edit: '手动编辑',
  45. EditIframe: '页内编辑',
  46. CloseIframe: '关闭编辑',
  47. CopySID: '复制脚本ID',
  48. Working: ['工作中...', '就快好了...'],
  49. InSetStatus: ['[ ]', '[✔]'],
  50. Error: {
  51. AlreadyExist: '脚本已经在此收藏集中了',
  52. NotExist: '脚本不在此收藏集中',
  53. Unknown: '未知错误'
  54. }
  55. },
  56. 'zh-TW': {
  57. FavEdit: '收藏集:',
  58. Add: '加入此集',
  59. Remove: '移出此集',
  60. Edit: '手動編輯',
  61. EditIframe: '頁內編輯',
  62. CloseIframe: '關閉編輯',
  63. CopySID: '複製腳本ID',
  64. Working: ['工作中...', '就快好了...'],
  65. InSetStatus: ['[ ]', '[✔]'],
  66. Error: {
  67. AlreadyExist: '腳本已經在此收藏集中了',
  68. NotExist: '腳本不在此收藏集中',
  69. Unknown: '未知錯誤'
  70. }
  71. },
  72. 'en': {
  73. FavEdit: 'Add to/Remove from favorite list: ',
  74. Add: 'Add',
  75. Remove: 'Remove',
  76. Edit: 'Edit Manually',
  77. EditIframe: 'In-Page Edit',
  78. CloseIframe: 'Close Editor',
  79. CopySID: 'Copy Script-ID',
  80. Working: ['Working...', 'Just a moment...'],
  81. InSetStatus: ['[ ]', '[✔]'],
  82. Error: {
  83. AlreadyExist: 'Script is already in set',
  84. NotExist: 'Script is not in set yet',
  85. Unknown: 'Unknown Error'
  86. }
  87. },
  88. 'default': {
  89. FavEdit: 'Add to/Remove from favorite list: ',
  90. Add: 'Add',
  91. Remove: 'Remove',
  92. Edit: 'Edit Manually',
  93. EditIframe: 'In-Page Edit',
  94. CloseIframe: 'Close Editor',
  95. CopySID: 'Copy Script-ID',
  96. Working: ['Working...', 'Just a moment...'],
  97. InSetStatus: ['[ ]', '[✔]'],
  98. Error: {
  99. AlreadyExist: 'Script is already in set',
  100. NotExist: 'Script is not in set yet',
  101. Unknown: 'Unknown Error'
  102. }
  103. },
  104. }
  105. }
  106.  
  107. // Get i18n code
  108. let i18n = $('#language-selector-locale') ? $('#language-selector-locale').value : navigator.language;
  109. if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}
  110.  
  111. main()
  112. function main() {
  113. const HOST = getHost();
  114. const API = getAPI();
  115.  
  116. // Common actions
  117. commons();
  118.  
  119. // API-based actions
  120. switch(API[1]) {
  121. case "scripts":
  122. API[2] && centerScript(API);
  123. break;
  124. default:
  125. DoLog('API is {}'.replace('{}', API));
  126. }
  127. }
  128.  
  129. function centerScript(API) {
  130. switch(API[3]) {
  131. case undefined:
  132. pageScript();
  133. break;
  134. case 'code':
  135. pageCode();
  136. break;
  137. case 'feedback':
  138. pageFeedback();
  139. break;
  140. }
  141. }
  142.  
  143. function commons() {
  144. // Your common actions here...
  145. GMXHRHook(5);
  146. }
  147.  
  148. function pageScript() {
  149. addFavPanel();
  150. }
  151.  
  152. function pageCode() {
  153. addFavPanel();
  154. }
  155.  
  156. function pageFeedback() {
  157. addFavPanel();
  158. }
  159.  
  160. function addFavPanel() {
  161. if (!getUserpage()) {return false;}
  162. GUI();
  163.  
  164. function GUI() {
  165. // Get elements
  166. const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');
  167. const script_parent = script_after.parentElement;
  168.  
  169. // My elements
  170. const script_favorite = $CrE('div');
  171. script_favorite.id = 'script-favorite';
  172. script_favorite.style.margin = '0.75em 0';
  173. script_favorite.innerHTML = CONST.Text[i18n].FavEdit;
  174.  
  175. const favorite_groups = $CrE('select');
  176. favorite_groups.id = 'favorite-groups';
  177.  
  178. const stored_sets = GM_getValue('script-sets', {sets: []}).sets;
  179. for (const set of stored_sets) {
  180. // Make <option>
  181. const option = $CrE('option');
  182. option.innerText = set.name;
  183. option.value = set.linkedit;
  184. $APD(favorite_groups, option);
  185. }
  186. adjustWidth();
  187.  
  188. refresh();
  189. favorite_groups.addEventListener('change', function(e) {
  190. favorite_edit.href = favorite_groups.value;
  191. });
  192.  
  193. const favorite_add = $CrE('a');
  194. favorite_add.id = 'favorite-add';
  195. favorite_add.innerHTML = CONST.Text[i18n].Add;
  196. favorite_add.style.margin = favorite_add.style.margin = '0px 0.5em';
  197. favorite_add.href = 'javascript:void(0);'
  198. favorite_add.addEventListener('click', e => addFav());
  199.  
  200. const favorite_remove = $CrE('a');
  201. favorite_remove.id = 'favorite-add';
  202. favorite_remove.innerHTML = CONST.Text[i18n].Remove;
  203. favorite_remove.style.margin = favorite_remove.style.margin = '0px 0.5em';
  204. favorite_remove.href = 'javascript:void(0);'
  205. favorite_remove.addEventListener('click', e => removeFav());
  206.  
  207. const favorite_edit = $CrE('a');
  208. favorite_edit.id = 'favorite-edit';
  209. favorite_edit.innerHTML = CONST.Text[i18n].Edit;
  210. favorite_edit.style.margin = favorite_edit.style.margin = '0px 0.5em';
  211. favorite_edit.target = '_blank';
  212.  
  213. const favorite_iframe = $CrE('a');
  214. favorite_iframe.id = 'favorite-edit-in-page';
  215. favorite_iframe.href = 'javascript: void(0);';
  216. favorite_iframe.innerHTML = CONST.Text[i18n].EditIframe;
  217. favorite_iframe.style.margin = favorite_iframe.style.margin = '0px 0.5em';
  218. favorite_iframe.target = '_blank';
  219. $AEL(favorite_iframe, 'click', editInPage);
  220.  
  221. const favorite_copy = $CrE('a');
  222. favorite_copy.id = 'favorite-copy';
  223. favorite_copy.href = 'javascript: void(0);';
  224. favorite_copy.innerHTML = CONST.Text[i18n].CopySID;
  225. favorite_copy.addEventListener('click', function() {
  226. copyText(getStrSID());
  227. });
  228.  
  229. // Append to document
  230. $APD(script_favorite, favorite_groups);
  231. script_parent.insertBefore(script_favorite, script_after);
  232. [favorite_add, favorite_remove, favorite_edit, favorite_iframe, favorite_copy].forEach(button => $APD(script_favorite, button));
  233.  
  234. function refresh() {
  235. getScriptSets(function(sets) {
  236. const old_value = favorite_groups.value;
  237. clearChildnodes(favorite_groups);
  238.  
  239. for (const set of sets) {
  240. // Make <option>
  241. const option = set.elmOption = $CrE('option');
  242. option.innerText = set.name;
  243. option.value = set.linkedit;
  244. $APD(favorite_groups, option);
  245. }
  246. adjustWidth();
  247.  
  248. // Recover selected <option>
  249. const selected = [...favorite_groups.children].find(option => option.value === old_value);
  250. selected && (selected.selected = true);
  251.  
  252. // Set edit-button.href
  253. favorite_edit.href = favorite_groups.value;
  254.  
  255. // Check script in-set status
  256. getInSets(sets, getStrSID(), inSets => {
  257. sets.forEach(set => {
  258. const inSet = inSets.includes(set);
  259. set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[inSet+0]} ${set.name}`;
  260. });
  261. adjustWidth();
  262. });
  263. })
  264. }
  265.  
  266. function adjustWidth() {
  267. favorite_groups.style.width = Math.max.apply(null, Array.from(favorite_groups.children).map((o) => (o.innerText.length))).toString() + 'em';
  268. favorite_groups.style.maxWidth = '40vw';
  269. }
  270.  
  271. function addFav() {
  272. const option = favorite_groups.selectedOptions[0];
  273. const set = GM_getValue('script-sets').sets.find(set => set.linkedit === option.value);
  274. const url = favorite_groups.value;
  275.  
  276. displayNotice(CONST.Text[i18n].Working[0]);
  277. modifyFav(favorite_groups.value, oDom => {
  278. const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === getStrSID());
  279. if (existingInput) {
  280. displayNotice(CONST.Text[i18n].Error.AlreadyExist);
  281. option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;
  282. return false;
  283. }
  284.  
  285. const input = $CrE('input');
  286. input.value = getStrSID();
  287. input.name = 'scripts-included[]';
  288. input.type = 'hidden';
  289. $APD($(oDom, '#script-set-scripts'), input);
  290. displayNotice(CONST.Text[i18n].Working[1]);
  291. }, oDom => {
  292. const status = $(oDom, 'p.notice');
  293. const status_text = status ? status.innerText : CONST.Text[i18n].Error.Unknown;
  294. displayNotice(status_text);
  295. option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;
  296. }, onerror);
  297. }
  298.  
  299. function removeFav() {
  300. const option = favorite_groups.selectedOptions[0];
  301. const set = GM_getValue('script-sets').sets.find(set => set.linkedit === option.value);
  302. const url = favorite_groups.value;
  303.  
  304. displayNotice(CONST.Text[i18n].Working[0]);
  305. modifyFav(favorite_groups.value, oDom => {
  306. const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === getStrSID());
  307. if (!existingInput) {
  308. displayNotice(CONST.Text[i18n].Error.NotExist);
  309. option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;
  310. return false;
  311. }
  312.  
  313. existingInput.remove();
  314. displayNotice(CONST.Text[i18n].Working[1]);
  315. }, oDom => {
  316. const status = $(oDom, 'p.notice');
  317. const status_text = status ? status.innerText : CONST.Text[i18n].Error.Unknown;
  318. displayNotice(status_text);
  319. option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;
  320. }, onerror);
  321. }
  322.  
  323. function modifyFav(url, editCallback, finishCallback, onerror) {
  324. getDocument(url, oDom => {
  325. if (editCallback(oDom) === false) {
  326. return false;
  327. }
  328. const form = $(oDom, '.change-script-set');
  329. const data = new FormData(form);
  330. data.append('save', '1');
  331.  
  332. /*
  333. GM_xmlhttpRequest({
  334. method: 'POST',
  335. url: toAbsoluteURL(form.getAttribute('action')),
  336. data,
  337. responseType: 'blob',
  338. onload: response => parseDocument(response.response, oDom => finishCallback(oDom)),
  339. onerror
  340. });
  341. */
  342. const xhr = new XMLHttpRequest();
  343. xhr.open('POST', toAbsoluteURL(form.getAttribute('action')));
  344. xhr.responseType = 'blob';
  345. xhr.onload = e => parseDocument(xhr.response, oDom => finishCallback(oDom));
  346. xhr.onerror = onerror;
  347. xhr.send(data);
  348. });
  349. }
  350.  
  351. function onerror() {
  352. displayNotice(CONST.Text[i18n].Error.Unknown);
  353. }
  354.  
  355. function editInPage(e) {
  356. e.preventDefault();
  357.  
  358. const _iframes = [...$All(script_favorite, '.script-edit-page')];
  359. if (_iframes.length) {
  360. // Iframe exists, close iframe
  361. favorite_iframe.innerText = CONST.Text[i18n].EditIframe;
  362. _iframes.forEach(ifr => ifr.remove());
  363. } else {
  364. // Iframe not exist, make iframe
  365. favorite_iframe.innerText = CONST.Text[i18n].CloseIframe;
  366.  
  367. const iframe = $$CrE({
  368. tagName: 'iframe',
  369. props: {
  370. src: favorite_groups.value
  371. },
  372. styles: {
  373. width: '100%',
  374. height: '60vh'
  375. },
  376. classes: ['script-edit-page'],
  377. listeners: [['load', e => {
  378. refresh();
  379. //iframe.style.height = iframe.contentDocument.body.parentElement.offsetHeight + 'px';
  380. }]]
  381. });
  382. script_favorite.appendChild(iframe);
  383. }
  384. }
  385.  
  386. function displayNotice(text) {
  387. const notice = $CrE('p');
  388. notice.classList.add('notice');
  389. notice.id = 'fav-notice';
  390. notice.innerText = text;
  391. const old_notice = $('#fav-notice');
  392. old_notice && old_notice.parentElement.removeChild(old_notice);
  393. $('#script-content').insertAdjacentElement('afterbegin', notice);
  394. }
  395. }
  396. }
  397.  
  398. function getScriptSets(callback, args=[]) {
  399. const userpage = getUserpage();
  400. getDocument(userpage, function(oDom) {
  401. /*
  402. const user_script_sets = oDom.querySelector('#user-script-sets');
  403. const script_sets = [];
  404.  
  405. for (const li of user_script_sets.querySelectorAll('li')) {
  406. // Get fav info
  407. const name = li.childNodes[0].nodeValue.trimRight();
  408. const link = li.children[0].href;
  409. const linkedit = li.children[1] ? li.children[1].href : 'https://greatest.deepsurf.us/' + $('#language-selector-locale').value + '/users/' + $('#nav-user-info>.user-profile-link>a').href.match(/[a-zA-Z\-]+\/users\/([^\/]*)/)[1] + '/sets/' + li.children[0].href.match(/[\?&]set=(\d+)/)[1] + '/edit';
  410.  
  411. // Append to script_sets
  412. script_sets.push({
  413. name: name,
  414. link: link,
  415. linkedit: linkedit
  416. });
  417. }
  418. */
  419. const script_sets = Array.from($(oDom, 'ul#user-script-sets').children).map(li => ({
  420. name: li.children[0].innerText,
  421. link: li.children[0].href,
  422. linkedit: li.children[1].href
  423. }));
  424.  
  425. // Save to GM_storage
  426. GM_setValue('script-sets', {
  427. sets: script_sets,
  428. time: (new Date()).getTime(),
  429. version: '0.2'
  430. });
  431.  
  432. // callback
  433. callback.apply(null, [script_sets].concat(args));
  434. });
  435. }
  436.  
  437. function getUserpage() {
  438. const a = $('#nav-user-info>.user-profile-link>a');
  439. return a ? a.href : null;
  440. }
  441.  
  442. function getInSet(set, sid, callback) {
  443. sid = sid.toString();
  444. getDocument(set.linkedit, oDom => {
  445. const inSet = [...$(oDom, '#script-set-scripts').children].some(input => input.value === sid);
  446. callback(inSet);
  447. });
  448. }
  449.  
  450. function getInSets(sets, sid, callback) {
  451. const AM = new AsyncManager();
  452. const inSets = [];
  453. for (const set of sets) {
  454. AM.add();
  455. getInSet(set, sid, inSet => {
  456. inSet && inSets.push(set);
  457. AM.finish();
  458. });
  459. }
  460. AM.onfinish = e => {
  461. callback(inSets);
  462. };
  463. AM.finishEvent = true;
  464. }
  465.  
  466. function getStrSID(url=location.href) {
  467. const API = getAPI(url);
  468. const strSID = API[2].match(/\d+/)[0];
  469. return strSID;
  470. }
  471.  
  472. function getSID(url=location.href) {
  473. return Number(getStrSID(url));
  474. }
  475. // Basic functions
  476. function $APD(a,b) {return a.appendChild(b);}
  477.  
  478. // Remove all childnodes from an element
  479. function clearChildnodes(element) {
  480. const cns = []
  481. for (const cn of element.childNodes) {
  482. cns.push(cn);
  483. }
  484. for (const cn of cns) {
  485. element.removeChild(cn);
  486. }
  487. }
  488.  
  489. function getDocumentXHR(url, callback, args=[]) {
  490. const xhr = new XMLHttpRequest();
  491. xhr.open('GET', url);
  492. xhr.responseType = 'blob';
  493. xhr.onloadstart = e => DoLog(LogLevel.Info, 'getting document with normal xhr, url=\'' + url + '\'');
  494. xhr.onload = e => {
  495. const htmlblob = xhr.response;
  496. parseDocument(htmlblob, callback, args);
  497. };
  498. xhr.send();
  499. }
  500.  
  501. // Download and parse a url page into a html document(dom).
  502. // when xhr onload: callback.apply([dom, args])
  503. function getDocument(url, callback, args=[], retry=5) {
  504. GM_xmlhttpRequest({
  505. method : 'GET',
  506. url : url,
  507. responseType : 'blob',
  508. onloadstart : function() {
  509. DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\'');
  510. },
  511. onload : function(response) {
  512. if (response.status === 200) {
  513. const htmlblob = response.response;
  514. parseDocument(htmlblob, callback, args);
  515. } else {
  516. re();
  517. }
  518. },
  519. onerror: e => re()
  520. });
  521.  
  522. function re() {
  523. DoLog(`Get document failed, retrying: (${retry}) ${url}`);
  524. --retry > 0 && getDocument(url, callback, args=[], retry);
  525. }
  526. }
  527.  
  528. function parseDocument(htmlblob, callback, args=[]) {
  529. const reader = new FileReader();
  530. reader.onload = function(e) {
  531. const htmlText = reader.result;
  532. const dom = new DOMParser().parseFromString(htmlText, 'text/html');
  533. args = [dom].concat(args);
  534. callback.apply(null, args);
  535. //callback(dom, htmlText);
  536. }
  537. reader.readAsText(htmlblob, document.characterSet);
  538. }
  539.  
  540. // Copy text to clipboard (needs to be called in an user event)
  541. function copyText(text) {
  542. // Create a new textarea for copying
  543. const newInput = document.createElement('textarea');
  544. document.body.appendChild(newInput);
  545. newInput.value = text;
  546. newInput.select();
  547. document.execCommand('copy');
  548. document.body.removeChild(newInput);
  549. }
  550.  
  551. // get '/' splited API array from a url
  552. function getAPI(url=location.href) {
  553. return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
  554. }
  555.  
  556. // get host part from a url(includes '^https://', '/$')
  557. function getHost(url=location.href) {
  558. const match = location.href.match(/https?:\/\/[^\/]+\//);
  559. return match ? match[0] : match;
  560. }
  561.  
  562. function toAbsoluteURL(relativeURL, base=`${location.protocol}//${location.host}/`) {
  563. return new URL(relativeURL, base).href;
  564. }
  565.  
  566. function randint(min, max) {
  567. return Math.floor(Math.random() * (max - min + 1)) + min;
  568. }
  569. })();