Greasyfork 快捷编辑收藏

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

Verze ze dne 07. 12. 2023. Zobrazit nejnovější verzi.

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