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