Greasyfork 快捷编辑收藏

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

2023-11-04 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

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