Greasyfork script-set-edit button

Add / Remove script into / from script set directly in GF script info page

  1. // ==UserScript==
  2. // @name Greasyfork script-set-edit button
  3. // @name:zh-CN Greasyfork 快捷编辑收藏
  4. // @name:zh-TW Greasyfork 快捷編輯收藏
  5. // @name:en Greasyfork script-set-edit button
  6. // @name:en-US Greasyfork script-set-edit button
  7. // @name:fr Greasyfork Set Edit+
  8. // @namespace Greasyfork-Favorite
  9. // @version 0.3.2.2
  10. // @description Add / Remove script into / from script set directly in GF script info page
  11. // @description:zh-CN 在GF脚本页直接编辑收藏集
  12. // @description:zh-TW 在GF腳本頁直接編輯收藏集
  13. // @description:en Add / Remove script into / from script set directly in GF script info page
  14. // @description:en-US Add / Remove script into / from script set directly in GF script info page
  15. // @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
  16. // @author PY-DNG
  17. // @license GPL-3.0-or-later
  18. // @match http*://*.greatest.deepsurf.us/*
  19. // @match http*://*.sleazyfork.org/*
  20. // @match http*://*.cn-greatest.deepsurf.us/*
  21. // @require https://update.greatest.deepsurf.us/scripts/456034/1597683/Basic%20Functions%20%28For%20userscripts%29.js
  22. // @require https://update.greatest.deepsurf.us/scripts/449583/1324274/ConfigManager.js
  23. // @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==
  24. // @grant GM_setValue
  25. // @grant GM_getValue
  26. // @grant GM_listValues
  27. // @grant GM_deleteValue
  28. // @grant GM_registerMenuCommand
  29. // @grant GM_unregisterMenuCommand
  30. // ==/UserScript==
  31.  
  32. /* eslint-disable no-multi-spaces */
  33. /* eslint-disable no-return-assign */
  34.  
  35. /* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager queueTask FunctionLoader loadFuncs require isLoaded */
  36. /* global ConfigManager */
  37.  
  38. (function __MAIN__() {
  39. 'use strict';
  40.  
  41. const CONST = {
  42. TextAllLang: {
  43. DEFAULT: 'en-US',
  44. 'zh-CN': {
  45. FavEdit: '收藏集:',
  46. Add: '加入此集',
  47. Remove: '移出此集',
  48. Edit: '手动编辑',
  49. EditIframe: '页内编辑',
  50. CloseIframe: '关闭编辑',
  51. CopySID: '复制脚本ID',
  52. Sync: '同步',
  53. NotLoggedIn: '请先登录Greasyfork',
  54. NoSetsYet: '您还没有创建过收藏集',
  55. NewSet: '新建收藏集',
  56. sortByApiDefault: ['默认排序', '默认倒序'],
  57. Working: ['工作中...', '就快好了...'],
  58. InSetStatus: ['[ ]', '[✔]'],
  59. Groups: {
  60. Server: 'GreasyFork收藏集',
  61. Local: '本地收藏集',
  62. New: '新建'
  63. },
  64. Refreshing: {
  65. List: '获取收藏集列表...',
  66. Script: '获取收藏集内容...',
  67. Data: '获取收藏集数据...'
  68. },
  69. UseAPI: ['[ ] 使用GF的收藏集API', '[✔]使用GF的收藏集API'],
  70. Error: {
  71. AlreadyExist: '脚本已经在此收藏集中了',
  72. NotExist: '脚本不在此收藏集中',
  73. NetworkError: '网络错误',
  74. Unknown: '未知错误'
  75. }
  76. },
  77. 'zh-TW': {
  78. FavEdit: '收藏集:',
  79. Add: '加入此集',
  80. Remove: '移出此集',
  81. Edit: '手動編輯',
  82. EditIframe: '頁內編輯',
  83. CloseIframe: '關閉編輯',
  84. CopySID: '複製腳本ID',
  85. Sync: '同步',
  86. NotLoggedIn: '請先登錄Greasyfork',
  87. NoSetsYet: '您還沒有創建過收藏集',
  88. NewSet: '新建收藏集',
  89. sortByApiDefault: ['默認排序', '默認倒序'],
  90. Working: ['工作中...', '就快好了...'],
  91. InSetStatus: ['[ ]', '[✔]'],
  92. Groups: {
  93. Server: 'GreasyFork收藏集',
  94. Local: '本地收藏集',
  95. New: '新建'
  96. },
  97. Refreshing: {
  98. List: '獲取收藏集清單...',
  99. Script: '獲取收藏集內容...',
  100. Data: '獲取收藏集數據...'
  101. },
  102. UseAPI: ['[ ] 使用GF的收藏集API', '[✔]使用GF的收藏集API'],
  103. Error: {
  104. AlreadyExist: '腳本已經在此收藏集中了',
  105. NotExist: '腳本不在此收藏集中',
  106. NetworkError: '網絡錯誤',
  107. Unknown: '未知錯誤'
  108. }
  109. },
  110. 'en-US': {
  111. FavEdit: 'Script set: ',
  112. Add: 'Add',
  113. Remove: 'Remove',
  114. Edit: 'Edit Manually',
  115. EditIframe: 'In-Page Edit',
  116. CloseIframe: 'Close Editor',
  117. CopySID: 'Copy Script-ID',
  118. Sync: 'Sync',
  119. NotLoggedIn: 'Login to greasyfork to use script sets',
  120. NoSetsYet: 'You haven\'t created a collection yet',
  121. NewSet: 'Create a new set',
  122. sortByApiDefault: ['Default', 'Default reverse'],
  123. Working: ['Working...', 'Just a moment...'],
  124. InSetStatus: ['[ ]', '[✔]'],
  125. Groups: {
  126. Server: 'GreasyFork',
  127. Local: 'Local',
  128. New: 'New'
  129. },
  130. Refreshing: {
  131. List: 'Fetching script sets...',
  132. Script: 'Fetching set content...',
  133. Data: 'Fetching script sets data...'
  134. },
  135. UseAPI: ['[ ] Use GF API', '[✔] Use GF API'],
  136. Error: {
  137. AlreadyExist: 'Script is already in set',
  138. NotExist: 'Script is not in set yet',
  139. NetworkError: 'Network Error',
  140. Unknown: 'Unknown Error'
  141. }
  142. },
  143. 'default': {
  144. FavEdit: 'Script set: ',
  145. Add: 'Add',
  146. Remove: 'Remove',
  147. Edit: 'Edit Manually',
  148. EditIframe: 'In-Page Edit',
  149. CloseIframe: 'Close Editor',
  150. CopySID: 'Copy Script-ID',
  151. Sync: 'Sync',
  152. NotLoggedIn: 'Login to greasyfork to use script sets',
  153. NoSetsYet: 'You haven\'t created a collection yet',
  154. NewSet: 'Create a new set',
  155. sortByApiDefault: ['Default', 'Default reverse'],
  156. Working: ['Working...', 'Just a moment...'],
  157. InSetStatus: ['[ ]', '[✔]'],
  158. Groups: {
  159. Server: 'GreasyFork',
  160. Local: 'Local',
  161. New: 'New'
  162. },
  163. Refreshing: {
  164. List: 'Fetching script sets...',
  165. Script: 'Fetching set content...',
  166. Data: 'Fetching script sets data...'
  167. },
  168. UseAPI: ['[ ] Use GF API', '[✔] Use GF API'],
  169. Error: {
  170. AlreadyExist: 'Script is already in set',
  171. NotExist: 'Script is not in set yet',
  172. NetworkError: 'Network Error',
  173. Unknown: 'Unknown Error'
  174. }
  175. },
  176. },
  177. URL: {
  178. SetLink: `https://${location.host}/scripts?set=$ID`,
  179. SetEdit: `https://${location.host}/users/$UID/sets/$ID/edit`
  180. },
  181. ConfigRule: {
  182. 'version-key': 'config-version',
  183. ignores: ['useAPI'],
  184. defaultValues: {
  185. 'script-sets': {
  186. sets: [],
  187. time: 0,
  188. 'config-version': 2,
  189. },
  190. 'useAPI': true
  191. },
  192. 'updaters': {
  193. /*'config-key': [
  194. function() {
  195. // This function contains updater for config['config-key'] from v0 to v1
  196. },
  197. function() {
  198. // This function contains updater for config['config-key'] from v1 to v2
  199. }
  200. ]*/
  201. 'script-sets': [
  202. config => {
  203. // v0 ==> v1
  204. // Fill set.id
  205. const sets = config.sets;
  206. sets.forEach(set => {
  207. const id = getUrlArgv(set.link, 'set');
  208. set.id = id;
  209. set.scripts = null; // After first refresh, it should be an array of SIDs:string
  210. });
  211.  
  212. // Delete old version identifier
  213. delete config.version;
  214.  
  215. return config;
  216. },
  217. config => {
  218. // v1 ==> v2
  219. return config
  220. }
  221. ]
  222. },
  223. },
  224. get Text() {
  225. const page_i18n = $('.language-selector-locale').value;
  226. const browser_i18n = navigator.language;
  227. return findLanguagePack(CONST.TextAllLang, page_i18n, browser_i18n);
  228.  
  229. function findLanguagePack(packs, ...lang_codes) {
  230. const getSuffix = code => code.slice(0, code.indexOf('-'));
  231. const lang_code =
  232. // Pack code that directly matches one of provided language code
  233. Object.keys(packs).find(pcode =>
  234. lang_codes.some(lcode => pcode === lcode)
  235. ) ??
  236. // Pack code which suffix matches one of provided language code
  237. Object.keys(packs).find(pcode =>
  238. lang_codes.some(lcode => getSuffix(pcode) === getSuffix(lcode))
  239. ) ??
  240. // No matches, use default language pack
  241. packs.DEFAULT;
  242. return packs[lang_code];
  243. }
  244. },
  245. };
  246.  
  247. const CM = new ConfigManager(CONST.ConfigRule);
  248. const CONFIG = CM.Config;
  249. CM.updateAllConfigs();
  250. CM.setDefaults();
  251.  
  252. const functions = {
  253. utils: {
  254. /** @typedef {Awaited<ReturnType<typeof functions.utils.func>>} utils */
  255. func() {
  256. function makeBooleanSettings(settings) {
  257. for (const setting of settings) {
  258. makeBooleanMenu(setting.text, setting.key, setting.defaultValue, setting.callback, setting.initCallback);
  259. }
  260.  
  261. function makeBooleanMenu(texts, key, defaultValue=false, callback=null, initCallback=false) {
  262. const initialVal = GM_getValue(key, defaultValue);
  263. const initialText = texts[initialVal + 0];
  264. let id = makeMenu(initialText, onClick);
  265. initCallback && callback(key, initialVal);
  266.  
  267. function onClick() {
  268. const newValue = !GM_getValue(key, defaultValue);
  269. const newText = texts[newValue + 0];
  270. GM_setValue(key, newValue);
  271. id = makeMenu(newText, onClick, id);
  272. typeof callback === 'function' && callback(key, newValue);
  273. }
  274.  
  275. function makeMenu(text, func, id) {
  276. if (GM_info.scriptHandler === 'Tampermonkey' && GM_hasVersion('5.0')) {
  277. return GM_registerMenuCommand(text, func, {
  278. id,
  279. autoClose: false,
  280. });
  281. } else {
  282. GM_unregisterMenuCommand(id);
  283. return GM_registerMenuCommand(text, func);
  284. }
  285. }
  286. }
  287.  
  288. function GM_hasVersion(version) {
  289. return hasVersion(GM_info?.version || '0', version);
  290.  
  291. function hasVersion(ver1, ver2) {
  292. return compareVersions(ver1.toString(), ver2.toString()) >= 0;
  293.  
  294. // https://greatest.deepsurf.us/app/javascript/versioncheck.js
  295. // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/format
  296. function compareVersions(a, b) {
  297. if (a == b) {
  298. return 0;
  299. }
  300. let aParts = a.split('.');
  301. let bParts = b.split('.');
  302. for (let i = 0; i < aParts.length; i++) {
  303. let result = compareVersionPart(aParts[i], bParts[i]);
  304. if (result != 0) {
  305. return result;
  306. }
  307. }
  308. // If all of a's parts are the same as b's parts, but b has additional parts, b is greater.
  309. if (bParts.length > aParts.length) {
  310. return -1;
  311. }
  312. return 0;
  313. }
  314.  
  315. function compareVersionPart(partA, partB) {
  316. let partAParts = parseVersionPart(partA);
  317. let partBParts = parseVersionPart(partB);
  318. for (let i = 0; i < partAParts.length; i++) {
  319. // "A string-part that exists is always less than a string-part that doesn't exist"
  320. if (partAParts[i].length > 0 && partBParts[i].length == 0) {
  321. return -1;
  322. }
  323. if (partAParts[i].length == 0 && partBParts[i].length > 0) {
  324. return 1;
  325. }
  326. if (partAParts[i] > partBParts[i]) {
  327. return 1;
  328. }
  329. if (partAParts[i] < partBParts[i]) {
  330. return -1;
  331. }
  332. }
  333. return 0;
  334. }
  335.  
  336. // It goes number, string, number, string. If it doesn't exist, then
  337. // 0 for numbers, empty string for strings.
  338. function parseVersionPart(part) {
  339. if (!part) {
  340. return [0, "", 0, ""];
  341. }
  342. let partParts = /([0-9]*)([^0-9]*)([0-9]*)([^0-9]*)/.exec(part)
  343. return [
  344. partParts[1] ? parseInt(partParts[1]) : 0,
  345. partParts[2],
  346. partParts[3] ? parseInt(partParts[3]) : 0,
  347. partParts[4]
  348. ];
  349. }
  350. }
  351. }
  352. }
  353.  
  354. // Copy text to clipboard (needs to be called in an user event)
  355. function copyText(text) {
  356. // Create a new textarea for copying
  357. const newInput = document.createElement('textarea');
  358. document.body.appendChild(newInput);
  359. newInput.value = text;
  360. newInput.select();
  361. document.execCommand('copy');
  362. document.body.removeChild(newInput);
  363. }
  364.  
  365. return {
  366. makeBooleanSettings,
  367. copyText
  368. }
  369. }
  370. },
  371. api: {
  372. /** @typedef {Awaited<ReturnType<typeof functions.api.func>>} api */
  373. func() {
  374. const API = {
  375. async getScriptSets() {
  376. const userpage = API.getUserpage();
  377. const oDom = await API.getDocument(userpage);
  378. const list = Array.from($(oDom, 'ul#user-script-sets').children);
  379. const NoSets = list.length === 1 && list.every(li => li.children.length === 1);
  380. const script_sets = NoSets ? [] : Array.from($(oDom, 'ul#user-script-sets').children).filter(li => li.children.length === 2).map(li => {
  381. try {
  382. return {
  383. name: li.children[0].innerText,
  384. link: li.children[0].href,
  385. linkedit: li.children[1].href,
  386. id: getUrlArgv(li.children[0].href, 'set')
  387. }
  388. } catch(err) {
  389. DoLog(LogLevel.Error, [li, err, li.children.length, li.children[0]?.innerHTML, li.children[1]?.innerHTML], 'error');
  390. Err(err);
  391. }
  392. });
  393. return script_sets;
  394. },
  395. async getSetScripts(url) {
  396. return [...$All(await API.getDocument(url), '#script-set-scripts>input[name="scripts-included[]"]')].map(input => input.value);
  397. },
  398. /**
  399. * @typedef {Object} SetsDataAPI
  400. * @property {Response} resp - api fetch response object
  401. * @property {boolean} ok - resp.ok (resp.status >= 200 && resp.status <= 299)
  402. * @property {(Object|null)} data - api response json data, or null if not resp.ok
  403. */
  404. /**
  405. * @returns {SetsDataAPI}
  406. */
  407. async getSetsData() {
  408. const userpage = API.getUserpage();
  409. const url = (userpage.endsWith('/') ? userpage : userpage + '/') + 'sets'
  410. const resp = await fetch(url, { credentials: 'same-origin' });
  411. if (resp.ok) {
  412. return {
  413. ok: true,
  414. resp,
  415. data: await resp.json()
  416. };
  417. } else {
  418. return {
  419. ok: false,
  420. resp,
  421. data: null
  422. };
  423. }
  424. },
  425. /**
  426. * @returns {(string|null)} the user's profile page url, from page top-right link <a>.href
  427. */
  428. getUserpage() {
  429. const a = $('#nav-user-info>.user-profile-link>a');
  430. return a ? a.href : null;
  431. },
  432. /**
  433. * @returns {(string|null)} the user's id, in string format
  434. */
  435. getUserID() {
  436. const userpage = API.getUserpage(); //https://greatest.deepsurf.us/zh-CN/users/667968-pyudng
  437. return userpage ? userpage.match(/\/users\/(\d+)(-[^\/]*\/*)?/)[1] : null;
  438. },
  439. // editCallback recieves:
  440. // true: edit doc load success
  441. // false: already in set
  442. // finishCallback recieves:
  443. // text: successfully added to set with text tip `text`
  444. // true: successfully loaded document but no text tip found
  445. // false: xhr error
  446. addFav(url, sid, editCallback, finishCallback) {
  447. API.modifyFav(url, oDom => {
  448. const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === sid);
  449. if (existingInput) {
  450. editCallback(false);
  451. return false;
  452. }
  453. const input = $CrE('input');
  454. input.value = sid;
  455. input.name = 'scripts-included[]';
  456. input.type = 'hidden';
  457. $(oDom, '#script-set-scripts').appendChild(input);
  458. editCallback(true);
  459. }, oDom => {
  460. const status = $(oDom, 'p.notice');
  461. const status_text = status ? status.innerText : true;
  462. finishCallback(status_text);
  463. }, err => finishCallback(false));
  464. },
  465. // editCallback recieves:
  466. // true: edit doc load success
  467. // false: already not in set
  468. // finishCallback recieves:
  469. // text: successfully removed from set with text tip `text`
  470. // true: successfully loaded document but no text tip found
  471. // false: xhr error
  472. removeFav(url, sid, editCallback, finishCallback) {
  473. API.modifyFav(url, oDom => {
  474. const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === sid);
  475. if (!existingInput) {
  476. editCallback(false);
  477. return false;
  478. }
  479. existingInput.remove();
  480. editCallback(true);
  481. }, oDom => {
  482. const status = $(oDom, 'p.notice');
  483. const status_text = status ? status.innerText : true;
  484. finishCallback(status_text);
  485. }, err => finishCallback(false));
  486. },
  487. async modifyFav(url, editCallback, finishCallback, onerror) {
  488. const oDom = await API.getDocument(url);
  489. if (editCallback(oDom) === false) { return false; }
  490. const form = $(oDom, '.change-script-set');
  491. const data = new FormData(form);
  492. data.append('save', '1');
  493. // Use XMLHttpRequest insteadof GM_xmlhttpRequest because there's unknown issue with GM_xmlhttpRequest
  494. const xhr = new XMLHttpRequest();
  495. xhr.open('POST', API.toAbsoluteURL(form.getAttribute('action')));
  496. xhr.responseType = 'blob';
  497. xhr.onload = async e => finishCallback(await API.parseDocument(xhr.response));
  498. xhr.onerror = onerror;
  499. xhr.send(data);
  500. },
  501. // Download and parse a url page into a html document(dom).
  502. // Returns a promise fulfills with dom
  503. async getDocument(url, retry=5) {
  504. try {
  505. const response = await fetch(url, {
  506. method: 'GET',
  507. cache: 'reload',
  508. });
  509. if (response.status === 200) {
  510. const blob = await response.blob();
  511. const oDom = await API.parseDocument(blob);
  512. return oDom;
  513. } else {
  514. throw new Error(`response.status is not 200 (${response.status})`);
  515. }
  516. } catch(err) {
  517. if (--retry > 0) {
  518. return API.getDocument(url, retry);
  519. } else {
  520. throw err;
  521. }
  522. }
  523. },
  524. // Returns a promise fulfills with dom
  525. parseDocument(htmlblob) {
  526. return new Promise((resolve, reject) => {
  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. resolve(dom);
  532. }
  533. reader.onerror = err => reject(err);
  534. reader.readAsText(htmlblob, document.characterSet);
  535. });
  536. },
  537. toAbsoluteURL(relativeURL, base=`${location.protocol}//${location.host}/`) {
  538. return new URL(relativeURL, base).href;
  539. },
  540. GM_hasVersion(version) {
  541. return hasVersion(GM_info?.version || '0', version);
  542. function hasVersion(ver1, ver2) {
  543. return compareVersions(ver1.toString(), ver2.toString()) >= 0;
  544. // https://greatest.deepsurf.us/app/javascript/versioncheck.js
  545. // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/format
  546. function compareVersions(a, b) {
  547. if (a == b) {
  548. return 0;
  549. }
  550. let aParts = a.split('.');
  551. let bParts = b.split('.');
  552. for (let i = 0; i < aParts.length; i++) {
  553. let result = compareVersionPart(aParts[i], bParts[i]);
  554. if (result != 0) {
  555. return result;
  556. }
  557. }
  558. // If all of a's parts are the same as b's parts, but b has additional parts, b is greater.
  559. if (bParts.length > aParts.length) {
  560. return -1;
  561. }
  562. return 0;
  563. }
  564. function compareVersionPart(partA, partB) {
  565. let partAParts = parseVersionPart(partA);
  566. let partBParts = parseVersionPart(partB);
  567. for (let i = 0; i < partAParts.length; i++) {
  568. // "A string-part that exists is always less than a string-part that doesn't exist"
  569. if (partAParts[i].length > 0 && partBParts[i].length == 0) {
  570. return -1;
  571. }
  572. if (partAParts[i].length == 0 && partBParts[i].length > 0) {
  573. return 1;
  574. }
  575. if (partAParts[i] > partBParts[i]) {
  576. return 1;
  577. }
  578. if (partAParts[i] < partBParts[i]) {
  579. return -1;
  580. }
  581. }
  582. return 0;
  583. }
  584. // It goes number, string, number, string. If it doesn't exist, then
  585. // 0 for numbers, empty string for strings.
  586. function parseVersionPart(part) {
  587. if (!part) {
  588. return [0, "", 0, ""];
  589. }
  590. let partParts = /([0-9]*)([^0-9]*)([0-9]*)([^0-9]*)/.exec(part)
  591. return [
  592. partParts[1] ? parseInt(partParts[1]) : 0,
  593. partParts[2],
  594. partParts[3] ? parseInt(partParts[3]) : 0,
  595. partParts[4]
  596. ];
  597. }
  598. }
  599. }
  600. };
  601. return API;
  602. }
  603. },
  604. 'Favorite panel': {
  605. checkers: {
  606. type: 'func',
  607. value: () => {
  608. const path = location.pathname.split('/').filter(p=>p).map(p => p.toLowerCase());
  609. const index = path.indexOf('scripts');
  610. const scripts_exist = [0,1].includes(index);
  611. const is_scripts_list = path.length-1 === index;
  612. const is_set_page = /[\?&]set=\d+/.test(location.search);
  613. const correct_page = [undefined, 'code', 'feedback'].includes(path[index+2]);
  614. return scripts_exist && !is_scripts_list && !is_set_page && correct_page;
  615. }
  616. },
  617. dependencies: ['utils', 'api'],
  618. func() {
  619. /** @type {utils} */
  620. const utils = require('utils');
  621. /** @type {api} */
  622. const GFScriptSetAPI = require('api');
  623. //if (!GFScriptSetAPI.getUserpage()) {return false;}
  624.  
  625. class FavoritePanel {
  626. #CM;
  627. #sid;
  628. #sets;
  629. #elements;
  630. #disabled;
  631.  
  632. constructor(CM) {
  633. this.#CM = CM;
  634. this.#sid = location.pathname.match(/scripts\/(\d+)/)[1];
  635. this.#sets = this.#CM.getConfig('script-sets').sets;
  636. this.#elements = {};
  637. this.disabled = false;
  638.  
  639. // Sort sets by name in alphabetical order
  640. FavoritePanel.#sortSetsdata(this.#sets);
  641.  
  642. const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');
  643. const script_parent = script_after.parentElement;
  644.  
  645. // Container
  646. const script_favorite = this.#elements.container = $$CrE({
  647. tagName: 'div',
  648. props: {
  649. id: 'script-favorite',
  650. innerHTML: CONST.Text.FavEdit
  651. },
  652. styles: { margin: '0.75em 0' }
  653. });
  654.  
  655. // Selecter
  656. const favorite_groups = this.#elements.select = $$CrE({
  657. tagName: 'select',
  658. props: { id: 'favorite-groups' },
  659. styles: { maxWidth: '40vw' },
  660. listeners: [['change', (() => {
  661. let lastSelected = 0;
  662. const record = () => lastSelected = favorite_groups.selectedIndex;
  663. const recover = () => favorite_groups.selectedIndex = lastSelected;
  664.  
  665. return e => {
  666. const value = favorite_groups.value;
  667. const type = /^\d+$/.test(value) ? 'set-id' : 'command';
  668.  
  669. switch (type) {
  670. case 'set-id': {
  671. const set = this.#sets.find(set => set.id === favorite_groups.value);
  672. favorite_edit.href = set.linkedit;
  673. break;
  674. }
  675. case 'command': {
  676. recover();
  677. this.#execCommand(value);
  678. }
  679. }
  680.  
  681. this.#refreshButtonDisplay();
  682. record();
  683. }
  684. }) ()]]
  685. });
  686. favorite_groups.id = 'favorite-groups';
  687.  
  688. // Buttons
  689. const makeBtn = (id, innerHTML, onClick, isLink=false) => $$CrE({
  690. tagName: 'a',
  691. props: {
  692. id, innerHTML,
  693. [isLink ? 'target' : 'href']: isLink ? '_blank' : 'javascript:void(0);'
  694. },
  695. styles: { margin: '0px 0.5em' },
  696. listeners: [['click', onClick]]
  697. });
  698.  
  699. const favorite_add = this.#elements.btnAdd = makeBtn('favorite-add', CONST.Text.Add, e => this.#addFav());
  700. const favorite_remove = this.#elements.btnRemove = makeBtn('favorite-remove', CONST.Text.Remove, e => this.#removeFav());
  701. const favorite_edit = this.#elements.btnEdit = makeBtn('favorite-edit', CONST.Text.Edit, e => {}, true);
  702. const favorite_iframe = this.#elements.btnIframe = makeBtn('favorite-edit-in-page', CONST.Text.EditIframe, e => this.#editInPage(e));
  703. const favorite_copy = this.#elements.btnCopy = makeBtn('favorite-add', CONST.Text.CopySID, e => utils.copyText(this.#sid));
  704. const favorite_sync = this.#elements.btnSync = makeBtn('favorite-sync', CONST.Text.Sync, e => this.#refresh());
  705.  
  706. script_favorite.appendChild(favorite_groups);
  707. script_after.before(script_favorite);
  708. [favorite_add, favorite_remove, favorite_edit, favorite_iframe, favorite_copy, favorite_sync].forEach(button => script_favorite.appendChild(button));
  709.  
  710. // Text tip
  711. const tip = this.#elements.tip = $CrE('span');
  712. script_favorite.appendChild(tip);
  713.  
  714. // Display cached sets first
  715. this.#displaySets();
  716.  
  717. // Request GF document to update sets
  718. this.#autoRefresh();
  719. }
  720.  
  721. get sid() {
  722. return this.#sid;
  723. }
  724.  
  725. get sets() {
  726. return FavoritePanel.#deepClone(this.#sets);
  727. }
  728.  
  729. get elements() {
  730. return FavoritePanel.#lightClone(this.#elements);
  731. }
  732.  
  733. #refresh() {
  734. const that = this;
  735. const method = CONFIG.useAPI ? 'api' : 'doc';
  736. return {
  737. api: () => this.#refresh_api(),
  738. doc: () => this.#refresh_doc()
  739. }[method]();
  740. }
  741.  
  742. async #refresh_api() {
  743. const CONFIG = this.#CM.Config;
  744.  
  745. this.#disable();
  746. this.#tip(CONST.Text.Refreshing.Data);
  747.  
  748. // Check login status
  749. if (!GFScriptSetAPI.getUserpage()) {
  750. this.#tip(CONST.Text.NotLoggedIn);
  751. return;
  752. }
  753.  
  754. // Request sets data api
  755. const api_result = await GFScriptSetAPI.getSetsData();
  756. const sets_data = api_result.data;
  757. const uid = GFScriptSetAPI.getUserID();
  758.  
  759. if (!api_result.ok) {
  760. // When api fails, use doc as fallback
  761. DoLog(LogLevel.Error, 'Sets API failed.');
  762. DoLog(LogLevel.Error, api_result);
  763. return this.#refresh_doc();
  764. }
  765.  
  766. // For forward compatibility, convert all setids and scriptids to string
  767. // and fill property set.link and set.linkedit
  768. for (const set of sets_data) {
  769. // convert set id to string
  770. set.id = set.id.toString();
  771. // https://greatest.deepsurf.us/zh-CN/scripts?set=439237
  772. set.link = replaceText(CONST.URL.SetLink, { $ID: set.id });
  773. // https://greatest.deepsurf.us/zh-CN/users/667968-pyudng/sets/439237/edit
  774. set.linkedit = replaceText(CONST.URL.SetEdit, { $UID: uid, $ID: set.id });
  775.  
  776. // there's two kind of sets: Favorite and non-favorite
  777. // favorite set's data is an array of object, where each object represents a script, with script's properties
  778. // non-favorite set's data is an array of ints, where each int means a script's id
  779. // For forward compatibility, we only store script ids, in string format
  780. set.scripts.forEach((script, i, scripts) => {
  781. if (typeof script === 'number') {
  782. scripts[i] = script.toString();
  783. } else {
  784. scripts[i] = script.id.toString();
  785. }
  786. });
  787. }
  788.  
  789. // Sort sets by name in alphabetical order
  790. FavoritePanel.#sortSetsdata(sets_data);
  791.  
  792. this.#sets = CONFIG['script-sets'].sets = sets_data;
  793. CONFIG['script-sets'].time = Date.now();
  794.  
  795. this.#tip();
  796. this.#enable();
  797. this.#displaySets();
  798. this.#refreshButtonDisplay();
  799. }
  800.  
  801. // Request document: get sets list and
  802. async #refresh_doc() {
  803. const CONFIG = this.#CM.Config;
  804.  
  805. this.#disable();
  806. this.#tip(CONST.Text.Refreshing.List);
  807.  
  808. // Check login status
  809. if (!GFScriptSetAPI.getUserpage()) {
  810. this.#tip(CONST.Text.NotLoggedIn);
  811. return;
  812. }
  813.  
  814. // Refresh sets list
  815. this.#sets = CONFIG['script-sets'].sets = await GFScriptSetAPI.getScriptSets();
  816. CONFIG['script-sets'].time = Date.now();
  817. this.#displaySets();
  818.  
  819. // Refresh each set's script list
  820. this.#tip(CONST.Text.Refreshing.Script);
  821. await Promise.all(this.#sets.map(async set => {
  822. // Fetch scripts
  823. set.scripts = await GFScriptSetAPI.getSetScripts(set.linkedit);
  824. this.#displaySets();
  825.  
  826. // Save to GM_storage
  827. const setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);
  828. CONFIG['script-sets'].sets[setIndex].scripts = set.scripts;
  829. CONFIG['script-sets'].time = Date.now();
  830. }));
  831.  
  832. this.#tip();
  833. this.#enable();
  834. this.#refreshButtonDisplay();
  835. }
  836.  
  837. // Refresh on instance creation.
  838. // This should be running in low-frequecy. Refreshing makes lots of requests which may resul in a 503 error(rate limit) for the user.
  839. #autoRefresh(minTime=1*24*60*60*1000) {
  840. const CONFIG = this.#CM.Config;
  841. const lastRefresh = new Date(CONFIG['script-sets'].time);
  842. if (Date.now() - lastRefresh > minTime) {
  843. this.#refresh();
  844. return true;
  845. } else {
  846. return false;
  847. }
  848. }
  849.  
  850. #addFav() {
  851. const set = this.#getCurrentSet();
  852. const option = set.elmOption;
  853.  
  854. this.#displayNotice(CONST.Text.Working[0]);
  855. GFScriptSetAPI.addFav(this.#getCurrentSet().linkedit, this.#sid, editStatus => {
  856. if (!editStatus) {
  857. this.#displayNotice(CONST.Text.Error.AlreadyExist);
  858. option.innerText = `${CONST.Text.InSetStatus[1]} ${set.name}`;
  859. } else {
  860. this.#displayNotice(CONST.Text.Working[1]);
  861. }
  862. }, finishStatus => {
  863. if (finishStatus) {
  864. // Save to this.#sets and GM_storage
  865. if (CONFIG['script-sets'].sets.some(set => !set.scripts)) {
  866. // If scripts property is missing, do sync(refresh)
  867. this.#refresh();
  868. } else {
  869. const setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);
  870. CONFIG['script-sets'].sets[setIndex].scripts.push(this.#sid);
  871. this.#sets = CM.getConfig('script-sets').sets;
  872. }
  873.  
  874. // Display
  875. this.#displayNotice(typeof finishStatus === 'string' ? finishStatus : CONST.Text.Error.Unknown);
  876. set.elmOption.innerText = `${CONST.Text.InSetStatus[1]} ${set.name}`;
  877. this.#displaySets();
  878. } else {
  879. this.#displayNotice(CONST.Text.Error.NetworkError);
  880. }
  881. });
  882. }
  883.  
  884. #removeFav() {
  885. const set = this.#getCurrentSet();
  886. const option = set.elmOption;
  887.  
  888. this.#displayNotice(CONST.Text.Working[0]);
  889. GFScriptSetAPI.removeFav(this.#getCurrentSet().linkedit, this.#sid, editStatus => {
  890. if (!editStatus) {
  891. this.#displayNotice(CONST.Text.Error.NotExist);
  892. option.innerText = `${CONST.Text.InSetStatus[0]} ${set.name}`;
  893. } else {
  894. this.#displayNotice(CONST.Text.Working[1]);
  895. }
  896. }, finishStatus => {
  897. if (finishStatus) {
  898. // Save to this.#sets and GM_storage
  899. if (CONFIG['script-sets'].sets.some(set => !set.scripts)) {
  900. // If scripts property is missing, do sync(refresh)
  901. this.#refresh();
  902. } else {
  903. const setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);
  904. const scriptIndex = CONFIG['script-sets'].sets[setIndex].scripts.indexOf(this.#sid);
  905. CONFIG['script-sets'].sets[setIndex].scripts.splice(scriptIndex, 1);
  906. this.#sets = CM.getConfig('script-sets').sets;
  907. }
  908.  
  909. // Display
  910. this.#displayNotice(typeof finishStatus === 'string' ? finishStatus : CONST.Text.Error.Unknown);
  911. set.elmOption.innerText = `${CONST.Text.InSetStatus[0]} ${set.name}`;
  912. this.#displaySets();
  913. } else {
  914. this.#displayNotice(CONST.Text.Error.NetworkError);
  915. }
  916. });
  917. }
  918.  
  919. #editInPage(e) {
  920. e.preventDefault();
  921.  
  922. const _iframes = [...$All(this.#elements.container, '.script-edit-page')];
  923. if (_iframes.length) {
  924. // Iframe exists, close iframe
  925. this.#elements.btnIframe.innerText = CONST.Text.EditIframe;
  926. _iframes.forEach(ifr => ifr.remove());
  927. this.#refresh();
  928. } else {
  929. // Iframe not exist, make iframe
  930. this.#elements.btnIframe.innerText = CONST.Text.CloseIframe;
  931.  
  932. const iframe = $$CrE({
  933. tagName: 'iframe',
  934. props: {
  935. src: this.#getCurrentSet().linkedit
  936. },
  937. styles: {
  938. width: '100%',
  939. height: '60vh'
  940. },
  941. classes: ['script-edit-page'],
  942. listeners: [['load', e => {
  943. //this.#refresh();
  944. //iframe.style.height = iframe.contentDocument.body.parentElement.offsetHeight + 'px';
  945. }]]
  946. });
  947. this.#elements.container.appendChild(iframe);
  948. }
  949. }
  950.  
  951. #displayNotice(text) {
  952. const notice = $CrE('p');
  953. notice.classList.add('notice');
  954. notice.id = 'fav-notice';
  955. notice.innerText = text;
  956. const old_notice = $('#fav-notice');
  957. old_notice && old_notice.parentElement.removeChild(old_notice);
  958. $('#script-content').insertAdjacentElement('afterbegin', notice);
  959. }
  960.  
  961. #tip(text='', timeout=0) {
  962. this.#elements.tip.innerText = text;
  963. timeout > 0 && setTimeout(() => this.#elements.tip.innerText = '', timeout);
  964. }
  965.  
  966. // Apply this.#sets to gui
  967. #displaySets() {
  968. const elements = this.#elements;
  969.  
  970. // Save selected set
  971. const old_value = elements.select.value;
  972. [...elements.select.children].forEach(child => child.remove());
  973.  
  974. // Make <optgroup>s and <option>s
  975. const serverGroup = elements.serverGroup = $$CrE({ tagName: 'optgroup', attrs: { label: CONST.Text.Groups.Server } });
  976. this.#sets.forEach(set => {
  977. // Create <option>
  978. set.elmOption = $$CrE({
  979. tagName: 'option',
  980. props: {
  981. innerText: set.name,
  982. value: set.id
  983. }
  984. });
  985. // Display inset status
  986. if (set.scripts) {
  987. const inSet = set.scripts.includes(this.#sid);
  988. set.elmOption.innerText = `${CONST.Text.InSetStatus[inSet+0]} ${set.name}`;
  989. }
  990. // Append <option> into <select>
  991. serverGroup.appendChild(set.elmOption);
  992. });
  993. if (this.#sets.length === 0) {
  994. const optEmpty = elements.optEmpty = $$CrE({
  995. tagName: 'option',
  996. props: {
  997. innerText: CONST.Text.NoSetsYet,
  998. value: 'empty',
  999. selected: true
  1000. }
  1001. });
  1002. serverGroup.appendChild(optEmpty);
  1003. }
  1004.  
  1005. const newGroup = elements.newGroup = $$CrE({ tagName: 'optgroup', attrs: { label: CONST.Text.Groups.New } });
  1006. const newSet = elements.newSet = $$CrE({
  1007. tagName: 'option',
  1008. props: {
  1009. innerText: CONST.Text.NewSet,
  1010. value: 'new',
  1011. }
  1012. });
  1013. newGroup.appendChild(newSet);
  1014. [serverGroup, newGroup].forEach(optgroup => elements.select.appendChild(optgroup));
  1015.  
  1016. // Adjust <select> width
  1017. elements.select.style.width = Math.max.apply(null, Array.from($All(elements.select, 'option')).map(o => o.innerText.length)).toString() + 'em';
  1018.  
  1019. // Select previous selected set's <option>
  1020. const selected = old_value ? [...$All(elements.select, 'option')].find(option => option.value === old_value) : null;
  1021. selected && (selected.selected = true);
  1022.  
  1023. // Set edit-button.href
  1024. if (elements.select.value !== 'empty') {
  1025. const curset = this.#sets.find(set => set.id === elements.select.value);
  1026. elements.btnEdit.href = curset.linkedit;
  1027. }
  1028.  
  1029. // Display correct button
  1030. this.#refreshButtonDisplay();
  1031. }
  1032.  
  1033. // Display only add button when script in current set, otherwise remove button
  1034. // Disable set-related buttons when not selecting options that not represents a set
  1035. #refreshButtonDisplay() {
  1036. const set = this.#getCurrentSet();
  1037. !this.#disabled && ([this.#elements.btnAdd, this.#elements.btnRemove, this.#elements.btnEdit, this.#elements.btnIframe]
  1038. .forEach(element => set ? FavoritePanel.#enableElement(element) : FavoritePanel.#disableElement(element)));
  1039. if (!set || !set.scripts) { return null; }
  1040. if (set.scripts.includes(this.#sid)) {
  1041. this.#elements.btnAdd.style.setProperty('display', 'none');
  1042. this.#elements.btnRemove.style.removeProperty('display');
  1043. return true;
  1044. } else {
  1045. this.#elements.btnRemove.style.setProperty('display', 'none');
  1046. this.#elements.btnAdd.style.removeProperty('display');
  1047. return false;
  1048. }
  1049. }
  1050.  
  1051. #execCommand(command) {
  1052. switch (command) {
  1053. case 'new': {
  1054. const url = GFScriptSetAPI.getUserpage() + (this.#getCurrentSet() ? '/sets/new' : '/sets/new?fav=1');
  1055. window.open(url);
  1056. break;
  1057. }
  1058. case 'empty': {
  1059. // Do nothing
  1060. break;
  1061. }
  1062. }
  1063. }
  1064.  
  1065. // Returns null if no <option>s yet
  1066. #getCurrentSet() {
  1067. return this.#sets.find(set => set.id === this.#elements.select.value) || null;
  1068. }
  1069.  
  1070. #disable() {
  1071. [
  1072. this.#elements.select,
  1073. this.#elements.btnAdd, this.#elements.btnRemove,
  1074. this.#elements.btnEdit, this.#elements.btnIframe,
  1075. this.#elements.btnCopy, this.#elements.btnSync
  1076. ].forEach(element => FavoritePanel.#disableElement(element));
  1077. this.#disabled = true;
  1078. }
  1079.  
  1080. #enable() {
  1081. [
  1082. this.#elements.select,
  1083. this.#elements.btnAdd, this.#elements.btnRemove,
  1084. this.#elements.btnEdit, this.#elements.btnIframe,
  1085. this.#elements.btnCopy, this.#elements.btnSync
  1086. ].forEach(element => FavoritePanel.#enableElement(element));
  1087. this.#disabled = false;
  1088. }
  1089.  
  1090. static #disableElement(element) {
  1091. element.style.filter = 'grayscale(1) brightness(0.95)';
  1092. element.style.opacity = '0.25';
  1093. element.style.pointerEvents = 'none';
  1094. element.tabIndex = -1;
  1095. }
  1096.  
  1097. static #enableElement(element) {
  1098. element.style.removeProperty('filter');
  1099. element.style.removeProperty('opacity');
  1100. element.style.removeProperty('pointer-events');
  1101. element.tabIndex = 0;
  1102. }
  1103.  
  1104. static #deepClone(val) {
  1105. if (typeof structuredClone === 'function') {
  1106. return structuredClone(val);
  1107. } else {
  1108. return JSON.parse(JSON.stringify(val));
  1109. }
  1110. }
  1111.  
  1112. static #lightClone(val) {
  1113. if (['string', 'number', 'boolean', 'undefined', 'bigint', 'symbol', 'function'].includes(val) || val === null) {
  1114. return val;
  1115. }
  1116. if (Array.isArray(val)) {
  1117. return val.slice();
  1118. }
  1119. if (typeof val === 'object') {
  1120. return Object.fromEntries(Object.entries(val));
  1121. }
  1122. }
  1123.  
  1124. static #sortSetsdata(sets_data) {
  1125. // Sort sets by name in alphabetical order
  1126. const sorted_names = sets_data.map(set => set.name).sort();
  1127. if (sorted_names.includes('Favorite')) {
  1128. // Keep set `Favorite` at first place
  1129. sorted_names.splice(0, 0, sorted_names.splice(sorted_names.indexOf('Favorite'), 1)[0]);
  1130. }
  1131. sets_data.sort((setA, setB) => sorted_names.indexOf(setA.name) - sorted_names.indexOf(setB.name));
  1132. }
  1133. }
  1134.  
  1135. const panel = new FavoritePanel(CM);
  1136. }
  1137. },
  1138. 'api-doc switch': {
  1139. checkers: {
  1140. type: 'switch',
  1141. value: true
  1142. },
  1143. dependencies: 'utils',
  1144. func: e => {
  1145. /** @type {utils} */
  1146. const utils = require('utils');
  1147. utils.makeBooleanSettings([{
  1148. text: CONST.Text.UseAPI,
  1149. key: 'useAPI',
  1150. defaultValue: true
  1151. }]);
  1152. }
  1153. },
  1154. 'Set scripts sort': {
  1155. checkers: {
  1156. type: 'func',
  1157. value: () => {
  1158. const scripts_exist = [1, 2].map(index => location.pathname.split('/')[index]?.toLowerCase()).includes('scripts');
  1159. const is_set_page = /[\?&]set=\d+/.test(location.search);
  1160. return scripts_exist && is_set_page;
  1161. }
  1162. },
  1163. detectDom: '#script-list-sort>ul',
  1164. func: e => {
  1165. const search = new URLSearchParams(location.search);
  1166. const set_id = search.get('set');
  1167. const sort = search.get('sort');
  1168. if (!CONFIG['script-sets'].sets.some(set => set.id === set_id)) { return false; }
  1169.  
  1170. const ul = $('#script-list-sort>ul');
  1171. [false, true].forEach(reverse => {
  1172. const li = $$CrE({
  1173. tagName: 'li',
  1174. classes: ['list-option', 'gse-sort'], // gse: (G)resyfork(S)et(E)dit+
  1175. attrs: { reverse: reverse ? '1' : '0' },
  1176. });
  1177. const a = $$CrE({
  1178. tagName: 'a',
  1179. props: { innerText: CONST.Text.sortByApiDefault[+reverse] },
  1180. attrs: { rel: 'nofollow', href: getSortUrl(reverse) }
  1181. });
  1182. li.appendChild(a);
  1183. ul.appendChild(li);
  1184. });
  1185. $AEL(ul, 'click', e => {
  1186. if (e.target.matches('.gse-sort>a')) {
  1187. e.preventDefault();
  1188. const a = e.target;
  1189. const li = a.parentElement;
  1190. const reverse = !!+li.getAttribute('reverse');
  1191. sortByApiDefault(reverse);
  1192. buttonClicked(a);
  1193. setSortUrl(reverse);
  1194. }
  1195. }, { capture: true });
  1196.  
  1197. switch (sort) {
  1198. case 'gse_default':
  1199. sortByApiDefault(false);
  1200. buttonClicked($('.gse-sort[reverse="0"]>a'));
  1201. break;
  1202. case 'gse_reverse':
  1203. sortByApiDefault(true);
  1204. buttonClicked($('.gse-sort[reverse="1"]>a'));
  1205. break;
  1206. }
  1207.  
  1208. /**
  1209. * Sort <li>s in #browse-script-list by default api order
  1210. * Default api order is by add-to-set time right now (2024-07-21),
  1211. * but this is not a promising feature
  1212. */
  1213. function sortByApiDefault(reverse=false) {
  1214. const ol = $('#browse-script-list');
  1215. const li_scripts = Array.from(ol.children);
  1216. const set = CM.getConfig('script-sets').sets.find(set => set.id === set_id);
  1217. const scripts = set.scripts;
  1218. li_scripts.sort((li1, li2) => {
  1219. const [sid1, sid2] = [li1, li2].map(li => li.getAttribute('data-script-id'));
  1220. const [index1, index2] = [sid1, sid2].map(sid => scripts.indexOf(sid)).map(index => index >= 0 ? index : Infinity);
  1221.  
  1222. return (reverse ? [1, -1] : [-1, 1])[index1 > index2 ? 1 : 0];
  1223. });
  1224. //li_scripts.forEach(li => ol.removeChild(li));
  1225. li_scripts.forEach(li => ol.appendChild(li));
  1226. }
  1227.  
  1228. /**
  1229. * Change the clicked button gui to given one
  1230. */
  1231. function buttonClicked(a) {
  1232. const li = a.parentElement;
  1233. const ul = li.parentElement;
  1234. const old_li_current = Array.from(ul.children).find(li => li.classList.contains('list-current'));
  1235.  
  1236. li.classList.add('list-current');
  1237. a.remove();
  1238. li.innerText = a.innerText;
  1239.  
  1240. old_li_current.classList.remove('list-current');
  1241. const old_li_a = $$CrE({
  1242. tagName: 'a',
  1243. attrs: { href: location.pathname + location.search + location.hash },
  1244. props: { innerText: old_li_current.innerText }
  1245. });
  1246. old_li_current.innerText = '';
  1247. old_li_current.appendChild(old_li_a);
  1248. }
  1249.  
  1250. /**
  1251. * Set url search params when sorting
  1252. */
  1253. function setSortUrl(reverse) {
  1254. history.replaceState({}, '', getSortUrl(reverse));
  1255. }
  1256.  
  1257. /**
  1258. * Make corrent url search params with sorting
  1259. */
  1260. function getSortUrl(reverse) {
  1261. const search = new URLSearchParams(location.search);
  1262. search.set('sort', reverse ? 'gse_reverse' : 'gse_default');
  1263. const url = location.pathname + '?' + search.toString();
  1264. return url;
  1265. }
  1266. }
  1267. }
  1268. };
  1269.  
  1270. const oFuncs = Object.entries(functions).reduce((arr, [id, oFunc]) => {
  1271. oFunc.id = id;
  1272. arr.push(oFunc);
  1273. return arr;
  1274. }, []);
  1275. loadFuncs(oFuncs);
  1276. })();