Greasyfork script-set-edit button

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

Verze ze dne 19. 02. 2024. Zobrazit nejnovější verzi.

  1. /* eslint-disable no-multi-spaces */
  2. /* eslint-disable no-return-assign */
  3.  
  4. // ==UserScript==
  5. // @name Greasyfork script-set-edit button
  6. // @name:zh-CN Greasyfork 快捷编辑收藏
  7. // @name:zh-TW Greasyfork 快捷編輯收藏
  8. // @name:en Greasyfork script-set-edit button
  9. // @name:en-US Greasyfork script-set-edit button
  10. // @name:fr Greasyfork Set Edit+
  11. // @namespace Greasyfork-Favorite
  12. // @version 0.2.4.2
  13. // @description Add / Remove script into / from script set directly in GF script info page
  14. // @description:zh-CN 在GF脚本页直接编辑收藏集
  15. // @description:zh-TW 在GF腳本頁直接編輯收藏集
  16. // @description:en Add / Remove script into / from script set directly in GF script info page
  17. // @description:en-US Add / Remove script into / from script set directly in GF script info page
  18. // @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
  19. // @author PY-DNG
  20. // @license GPL-3.0-or-later
  21. // @match http*://*.greatest.deepsurf.us/*
  22. // @match http*://*.sleazyfork.org/*
  23. // @match http*://greatest.deepsurf.us/*
  24. // @match http*://sleazyfork.org/*
  25. // @require https://update.greatest.deepsurf.us/scripts/456034/1303041/Basic%20Functions%20%28For%20userscripts%29.js
  26. // @require https://update.greatest.deepsurf.us/scripts/449583/1324274/ConfigManager.js
  27. // @require https://greatest.deepsurf.us/scripts/460385-gm-web-hooks/code/script.js?version=1221394
  28. // @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==
  29. // @grant GM_xmlhttpRequest
  30. // @grant GM_setValue
  31. // @grant GM_getValue
  32. // @grant GM_listValues
  33. // @grant GM_deleteValue
  34. // ==/UserScript==
  35.  
  36. /* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */
  37. /* global GMXHRHook GMDLHook ConfigManager */
  38.  
  39. const GFScriptSetAPI = (function() {
  40. const API = {
  41. async getScriptSets() {
  42. const userpage = API.getUserpage();
  43. const oDom = await API.getDocument(userpage);
  44.  
  45. const script_sets = Array.from($(oDom, 'ul#user-script-sets').children).map(li => {
  46. try {
  47. return {
  48. name: li.children[0].innerText,
  49. link: li.children[0].href,
  50. linkedit: li.children[1].href,
  51. id: getUrlArgv(li.children[0].href, 'set')
  52. }
  53. } catch(err) {
  54. DoLog(LogLevel.Error, [li, err, li.children.length, li.children[0]?.innerHTML, li.children[1]?.innerHTML], 'error');
  55. Err(err);
  56. }
  57. });
  58.  
  59. return script_sets;
  60. },
  61.  
  62. async getSetScripts(url) {
  63. return [...$All(await API.getDocument(url), '#script-set-scripts>input[name="scripts-included[]"]')].map(input => input.value);
  64. },
  65.  
  66. getUserpage() {
  67. const a = $('#nav-user-info>.user-profile-link>a');
  68. return a ? a.href : null;
  69. },
  70.  
  71. // editCallback recieves:
  72. // true: edit doc load success
  73. // false: already in set
  74. // finishCallback recieves:
  75. // text: successfully added to set with text tip `text`
  76. // true: successfully loaded document but no text tip found
  77. // false: xhr error
  78. addFav(url, sid, editCallback, finishCallback) {
  79. API.modifyFav(url, oDom => {
  80. const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === sid);
  81. if (existingInput) {
  82. editCallback(false);
  83. return false;
  84. }
  85.  
  86. const input = $CrE('input');
  87. input.value = sid;
  88. input.name = 'scripts-included[]';
  89. input.type = 'hidden';
  90. $(oDom, '#script-set-scripts').appendChild(input);
  91. editCallback(true);
  92. }, oDom => {
  93. const status = $(oDom, 'p.notice');
  94. const status_text = status ? status.innerText : true;
  95. finishCallback(status_text);
  96. }, err => finishCallback(false));
  97. },
  98.  
  99. // editCallback recieves:
  100. // true: edit doc load success
  101. // false: already not in set
  102. // finishCallback recieves:
  103. // text: successfully removed from set with text tip `text`
  104. // true: successfully loaded document but no text tip found
  105. // false: xhr error
  106. removeFav(url, sid, editCallback, finishCallback) {
  107. API.modifyFav(url, oDom => {
  108. const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === sid);
  109. if (!existingInput) {
  110. editCallback(false);
  111. return false;
  112. }
  113.  
  114. existingInput.remove();
  115. editCallback(true);
  116. }, oDom => {
  117. const status = $(oDom, 'p.notice');
  118. const status_text = status ? status.innerText : true;
  119. finishCallback(status_text);
  120. }, err => finishCallback(false));
  121. },
  122.  
  123. async modifyFav(url, editCallback, finishCallback, onerror) {
  124. const oDom = await API.getDocument(url);
  125. if (editCallback(oDom) === false) { return false; }
  126.  
  127. const form = $(oDom, '.change-script-set');
  128. const data = new FormData(form);
  129. data.append('save', '1');
  130.  
  131. // Use XMLHttpRequest insteadof GM_xmlhttpRequest because there's unknown issue with GM_xmlhttpRequest
  132. // Use XMLHttpRequest insteadof GM_xmlhttpRequest before Tampermonkey 5.0.0 because of FormData posting issues
  133. if (true || GM_info.scriptHandler === 'Tampermonkey' && !API.GM_hasVersion('5.0')) {
  134. const xhr = new XMLHttpRequest();
  135. xhr.open('POST', API.toAbsoluteURL(form.getAttribute('action')));
  136. xhr.responseType = 'blob';
  137. xhr.onload = async e => finishCallback(await API.parseDocument(xhr.response));
  138. xhr.onerror = onerror;
  139. xhr.send(data);
  140. } else {
  141. GM_xmlhttpRequest({
  142. method: 'POST',
  143. url: API.toAbsoluteURL(form.getAttribute('action')),
  144. data,
  145. responseType: 'blob',
  146. onload: async response => finishCallback(await API.parseDocument(response.response)),
  147. onerror
  148. });
  149. }
  150. },
  151.  
  152. // Download and parse a url page into a html document(dom).
  153. // Returns a promise fulfills with dom
  154. getDocument(url, retry=5) {
  155. return new Promise((resolve, reject) => {
  156. GM_xmlhttpRequest({
  157. method : 'GET',
  158. url : url,
  159. responseType : 'blob',
  160. onload : function(response) {
  161. if (response.status === 200) {
  162. const htmlblob = response.response;
  163. API.parseDocument(htmlblob).then(resolve).catch(reject);
  164. } else {
  165. re(response);
  166. }
  167. },
  168. onerror: err => re(err)
  169. });
  170.  
  171. function re(err) {
  172. DoLog(`Get document failed, retrying: (${retry}) ${url}`);
  173. --retry > 0 ? API.getDocument(url, retry).then(resolve).catch(reject) : reject(err);
  174. }
  175. });
  176. },
  177.  
  178. // Returns a promise fulfills with dom
  179. parseDocument(htmlblob) {
  180. return new Promise((resolve, reject) => {
  181. const reader = new FileReader();
  182. reader.onload = function(e) {
  183. const htmlText = reader.result;
  184. const dom = new DOMParser().parseFromString(htmlText, 'text/html');
  185. resolve(dom);
  186. }
  187. reader.onerror = err => reject(err);
  188. reader.readAsText(htmlblob, document.characterSet);
  189. });
  190. },
  191.  
  192. toAbsoluteURL(relativeURL, base=`${location.protocol}//${location.host}/`) {
  193. return new URL(relativeURL, base).href;
  194. },
  195.  
  196. GM_hasVersion(version) {
  197. return hasVersion(GM_info?.version || '0', version);
  198.  
  199. function hasVersion(ver1, ver2) {
  200. return compareVersions(ver1.toString(), ver2.toString()) >= 0;
  201.  
  202. // https://greatest.deepsurf.us/app/javascript/versioncheck.js
  203. // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/format
  204. function compareVersions(a, b) {
  205. if (a == b) {
  206. return 0;
  207. }
  208. let aParts = a.split('.');
  209. let bParts = b.split('.');
  210. for (let i = 0; i < aParts.length; i++) {
  211. let result = compareVersionPart(aParts[i], bParts[i]);
  212. if (result != 0) {
  213. return result;
  214. }
  215. }
  216. // If all of a's parts are the same as b's parts, but b has additional parts, b is greater.
  217. if (bParts.length > aParts.length) {
  218. return -1;
  219. }
  220. return 0;
  221. }
  222.  
  223. function compareVersionPart(partA, partB) {
  224. let partAParts = parseVersionPart(partA);
  225. let partBParts = parseVersionPart(partB);
  226. for (let i = 0; i < partAParts.length; i++) {
  227. // "A string-part that exists is always less than a string-part that doesn't exist"
  228. if (partAParts[i].length > 0 && partBParts[i].length == 0) {
  229. return -1;
  230. }
  231. if (partAParts[i].length == 0 && partBParts[i].length > 0) {
  232. return 1;
  233. }
  234. if (partAParts[i] > partBParts[i]) {
  235. return 1;
  236. }
  237. if (partAParts[i] < partBParts[i]) {
  238. return -1;
  239. }
  240. }
  241. return 0;
  242. }
  243.  
  244. // It goes number, string, number, string. If it doesn't exist, then
  245. // 0 for numbers, empty string for strings.
  246. function parseVersionPart(part) {
  247. if (!part) {
  248. return [0, "", 0, ""];
  249. }
  250. let partParts = /([0-9]*)([^0-9]*)([0-9]*)([^0-9]*)/.exec(part)
  251. return [
  252. partParts[1] ? parseInt(partParts[1]) : 0,
  253. partParts[2],
  254. partParts[3] ? parseInt(partParts[3]) : 0,
  255. partParts[4]
  256. ];
  257. }
  258. }
  259. }
  260. };
  261.  
  262. return API;
  263. }) ();
  264.  
  265. (function __MAIN__() {
  266. 'use strict';
  267.  
  268. const CONST = {
  269. Text: {
  270. 'zh-CN': {
  271. FavEdit: '收藏集:',
  272. Add: '加入此集',
  273. Remove: '移出此集',
  274. Edit: '手动编辑',
  275. EditIframe: '页内编辑',
  276. CloseIframe: '关闭编辑',
  277. CopySID: '复制脚本ID',
  278. Sync: '同步',
  279. Working: ['工作中...', '就快好了...'],
  280. InSetStatus: ['[ ]', '[✔]'],
  281. Refreshing: {
  282. List: '获取收藏集列表...',
  283. Script: '获取收藏集内容...'
  284. },
  285. Error: {
  286. AlreadyExist: '脚本已经在此收藏集中了',
  287. NotExist: '脚本不在此收藏集中',
  288. NetworkError: '网络错误',
  289. Unknown: '未知错误'
  290. }
  291. },
  292. 'zh-TW': {
  293. FavEdit: '收藏集:',
  294. Add: '加入此集',
  295. Remove: '移出此集',
  296. Edit: '手動編輯',
  297. EditIframe: '頁內編輯',
  298. CloseIframe: '關閉編輯',
  299. CopySID: '複製腳本ID',
  300. Sync: '同步',
  301. Working: ['工作中...', '就快好了...'],
  302. InSetStatus: ['[ ]', '[✔]'],
  303. Refreshing: {
  304. List: '獲取收藏集清單...',
  305. Script: '獲取收藏集內容...'
  306. },
  307. Error: {
  308. AlreadyExist: '腳本已經在此收藏集中了',
  309. NotExist: '腳本不在此收藏集中',
  310. NetworkError: '網絡錯誤',
  311. Unknown: '未知錯誤'
  312. }
  313. },
  314. 'en': {
  315. FavEdit: 'Script set: ',
  316. Add: 'Add',
  317. Remove: 'Remove',
  318. Edit: 'Edit Manually',
  319. EditIframe: 'In-Page Edit',
  320. CloseIframe: 'Close Editor',
  321. CopySID: 'Copy Script-ID',
  322. Sync: 'Sync',
  323. Working: ['Working...', 'Just a moment...'],
  324. InSetStatus: ['[ ]', '[✔]'],
  325. Refreshing: {
  326. List: 'Fetching script sets...',
  327. Script: 'Fetching set content...'
  328. },
  329. Error: {
  330. AlreadyExist: 'Script is already in set',
  331. NotExist: 'Script is not in set yet',
  332. NetworkError: 'Network Error',
  333. Unknown: 'Unknown Error'
  334. }
  335. },
  336. 'default': {
  337. FavEdit: 'Script set: ',
  338. Add: 'Add',
  339. Remove: 'Remove',
  340. Edit: 'Edit Manually',
  341. EditIframe: 'In-Page Edit',
  342. CloseIframe: 'Close Editor',
  343. CopySID: 'Copy Script-ID',
  344. Sync: 'Sync',
  345. Working: ['Working...', 'Just a moment...'],
  346. InSetStatus: ['[ ]', '[✔]'],
  347. Refreshing: {
  348. List: 'Fetching script sets...',
  349. Script: 'Fetching set content...'
  350. },
  351. Error: {
  352. AlreadyExist: 'Script is already in set',
  353. NotExist: 'Script is not in set yet',
  354. NetworkError: 'Network Error',
  355. Unknown: 'Unknown Error'
  356. }
  357. },
  358. },
  359. ConfigRule: {
  360. 'version-key': 'config-version',
  361. ignores: [],
  362. defaultValues: {
  363. 'script-sets': {
  364. 'config-version': 1,
  365. },
  366. },
  367. 'updaters': {
  368. /*'config-key': [
  369. function() {
  370. // This function contains updater for config['config-key'] from v0 to v1
  371. },
  372. function() {
  373. // This function contains updater for config['config-key'] from v1 to v2
  374. }
  375. ]*/
  376. 'script-sets': [
  377. config => {
  378. // Fill set.id
  379. const sets = config.sets;
  380. sets.forEach(set => {
  381. const id = getUrlArgv(set.link, 'set');
  382. set.id = id;
  383. set.scripts = null; // After first refresh, it should be an array of SIDs:string
  384. });
  385.  
  386. // Delete old version identifier
  387. delete config.version;
  388.  
  389. return config;
  390. }
  391. ]
  392. },
  393. }
  394. }
  395.  
  396. // Get i18n code
  397. let i18n = $('#language-selector-locale') ? $('#language-selector-locale').value : navigator.language;
  398. if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}
  399.  
  400. const CM = new ConfigManager(CONST.ConfigRule);
  401. const CONFIG = CM.Config;
  402. CM.updateAllConfigs();
  403.  
  404. loadFuncs([{
  405. name: 'Hook GM_xmlhttpRequest',
  406. checker: {
  407. type: 'switch',
  408. value: true
  409. },
  410. func: () => GMXHRHook(5)
  411. }, {
  412. name: 'Favorite panel',
  413. checker: {
  414. type: 'func',
  415. value: () => {
  416. const path = location.pathname.split('/').filter(p=>p);
  417. const index = path.indexOf('scripts');
  418. return [0,1].includes(index) && [undefined, 'code', 'feedback'].includes(path[index+2])
  419. }
  420. },
  421. func: addFavPanel
  422. }]);
  423.  
  424. function addFavPanel() {
  425. if (!GFScriptSetAPI.getUserpage()) {return false;}
  426.  
  427. class FavoritePanel {
  428. #CM;
  429. #sid;
  430. #sets;
  431. #elements;
  432.  
  433. constructor(CM) {
  434. this.#CM = CM;
  435. this.#sid = location.pathname.match(/scripts\/(\d+)/)[1];
  436. this.#sets = this.#CM.getConfig('script-sets').sets;
  437. this.#elements = {};
  438.  
  439. const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');
  440. const script_parent = script_after.parentElement;
  441.  
  442. // Container
  443. const script_favorite = this.#elements.container = $$CrE({
  444. tagName: 'div',
  445. props: {
  446. id: 'script-favorite',
  447. innerHTML: CONST.Text[i18n].FavEdit
  448. },
  449. styles: { margin: '0.75em 0' }
  450. });
  451.  
  452. // Selecter
  453. const favorite_groups = this.#elements.groups = $$CrE({
  454. tagName: 'select',
  455. props: { id: 'favorite-groups' },
  456. styles: { maxWidth: '40vw' },
  457. listeners: [['change', e => {
  458. const set = this.#sets.find(set => set.id === favorite_groups.value);
  459. favorite_edit.href = set.linkedit;
  460. this.#refreshButtonDisplay();
  461. }]]
  462. });
  463. favorite_groups.id = 'favorite-groups';
  464.  
  465. // Buttons
  466. const makeBtn = (id, innerHTML, onClick, isLink=false) => $$CrE({
  467. tagName: 'a',
  468. props: {
  469. id, innerHTML,
  470. [isLink ? 'target' : 'href']: isLink ? '_blank' : 'javascript:void(0);'
  471. },
  472. styles: { margin: '0px 0.5em' },
  473. listeners: [['click', onClick]]
  474. });
  475.  
  476. const favorite_add = this.#elements.btnAdd = makeBtn('favorite-add', CONST.Text[i18n].Add, e => this.#addFav());
  477. const favorite_remove = this.#elements.btnRemove = makeBtn('favorite-remove', CONST.Text[i18n].Remove, e => this.#removeFav());
  478. const favorite_edit = this.#elements.btnEdit = makeBtn('favorite-edit', CONST.Text[i18n].Edit, e => {}, true);
  479. const favorite_iframe = this.#elements.btnIframe = makeBtn('favorite-edit-in-page', CONST.Text[i18n].EditIframe, e => this.#editInPage(e));
  480. const favorite_copy = this.#elements.btnCopy = makeBtn('favorite-add', CONST.Text[i18n].CopySID, e => copyText(this.#sid));
  481. const favorite_sync = this.#elements.btnSync = makeBtn('favorite-sync', CONST.Text[i18n].Sync, e => this.#refresh());
  482.  
  483. script_favorite.appendChild(favorite_groups);
  484. script_after.before(script_favorite);
  485. [favorite_add, favorite_remove, favorite_edit, favorite_iframe, favorite_copy, favorite_sync].forEach(button => script_favorite.appendChild(button));
  486.  
  487. // Text tip
  488. const tip = this.#elements.tip = $CrE('span');
  489. script_favorite.appendChild(tip);
  490.  
  491. // Display cached sets first
  492. this.#displaySets();
  493.  
  494. // Request GF document to update sets
  495. this.#refresh();
  496. }
  497.  
  498. get sid() {
  499. return this.#sid;
  500. }
  501.  
  502. get sets() {
  503. return FavoritePanel.#deepClone(this.#sets);
  504. }
  505.  
  506. get elements() {
  507. return FavoritePanel.#lightClone(this.#elements);
  508. }
  509.  
  510. // Request document: get sets list and
  511. async #refresh() {
  512. this.#disable();
  513. this.#tip(CONST.Text[i18n].Refreshing.List);
  514.  
  515. // Refresh sets list
  516. this.#sets = CONFIG['script-sets'].sets = await GFScriptSetAPI.getScriptSets();
  517. this.#displaySets();
  518.  
  519. // Refresh each set's script list
  520. this.#tip(CONST.Text[i18n].Refreshing.Script);
  521. await Promise.all(this.#sets.map(async set => {
  522. // Fetch scripts
  523. set.scripts = await GFScriptSetAPI.getSetScripts(set.linkedit);
  524. this.#displaySets();
  525.  
  526. // Save to GM_storage
  527. const setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);
  528. CONFIG['script-sets'].sets[setIndex].scripts = set.scripts;
  529. }));
  530.  
  531. this.#tip();
  532. this.#enable();
  533. }
  534.  
  535. #addFav() {
  536. const set = this.#getCurrentSet();
  537. const option = set.elmOption;
  538.  
  539. this.#displayNotice(CONST.Text[i18n].Working[0]);
  540. GFScriptSetAPI.addFav(this.#getCurrentSet().linkedit, this.#sid, editStatus => {
  541. if (!editStatus) {
  542. this.#displayNotice(CONST.Text[i18n].Error.AlreadyExist);
  543. option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;
  544. } else {
  545. this.#displayNotice(CONST.Text[i18n].Working[1]);
  546. }
  547. }, finishStatus => {
  548. if (finishStatus) {
  549. // Save to this.#sets and GM_storage
  550. const setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);
  551. CONFIG['script-sets'].sets[setIndex].scripts.push(this.#sid);
  552. this.#sets = CM.getConfig('script-sets').sets;
  553.  
  554. // Display
  555. this.#displayNotice(typeof finishStatus === 'string' ? finishStatus : CONST.Text[i18n].Error.Unknown);
  556. set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;
  557. this.#displaySets();
  558. } else {
  559. this.#displayNotice(CONST.Text[i18n].Error.NetworkError);
  560. }
  561. });
  562. }
  563.  
  564. #removeFav() {
  565. const set = this.#getCurrentSet();
  566. const option = set.elmOption;
  567.  
  568. this.#displayNotice(CONST.Text[i18n].Working[0]);
  569. GFScriptSetAPI.removeFav(this.#getCurrentSet().linkedit, this.#sid, editStatus => {
  570. if (!editStatus) {
  571. this.#displayNotice(CONST.Text[i18n].Error.NotExist);
  572. option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;
  573. } else {
  574. this.#displayNotice(CONST.Text[i18n].Working[1]);
  575. }
  576. }, finishStatus => {
  577. if (finishStatus) {
  578. // Save to this.#sets and GM_storage
  579. const setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);
  580. const scriptIndex = CONFIG['script-sets'].sets[setIndex].scripts.indexOf(this.#sid);
  581. CONFIG['script-sets'].sets[setIndex].scripts.splice(scriptIndex, 1);
  582. this.#sets = CM.getConfig('script-sets').sets;
  583.  
  584. // Display
  585. this.#displayNotice(typeof finishStatus === 'string' ? finishStatus : CONST.Text[i18n].Error.Unknown);
  586. set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;
  587. this.#displaySets();
  588. } else {
  589. this.#displayNotice(CONST.Text[i18n].Error.NetworkError);
  590. }
  591. });
  592. }
  593.  
  594. #editInPage(e) {
  595. e.preventDefault();
  596.  
  597. const _iframes = [...$All(this.#elements.container, '.script-edit-page')];
  598. if (_iframes.length) {
  599. // Iframe exists, close iframe
  600. this.#elements.btnIframe.innerText = CONST.Text[i18n].EditIframe;
  601. _iframes.forEach(ifr => ifr.remove());
  602. this.#refresh();
  603. } else {
  604. // Iframe not exist, make iframe
  605. this.#elements.btnIframe.innerText = CONST.Text[i18n].CloseIframe;
  606.  
  607. const iframe = $$CrE({
  608. tagName: 'iframe',
  609. props: {
  610. src: this.#getCurrentSet().linkedit
  611. },
  612. styles: {
  613. width: '100%',
  614. height: '60vh'
  615. },
  616. classes: ['script-edit-page'],
  617. listeners: [['load', e => {
  618. //this.#refresh();
  619. //iframe.style.height = iframe.contentDocument.body.parentElement.offsetHeight + 'px';
  620. }]]
  621. });
  622. this.#elements.container.appendChild(iframe);
  623. }
  624. }
  625.  
  626. #displayNotice(text) {
  627. const notice = $CrE('p');
  628. notice.classList.add('notice');
  629. notice.id = 'fav-notice';
  630. notice.innerText = text;
  631. const old_notice = $('#fav-notice');
  632. old_notice && old_notice.parentElement.removeChild(old_notice);
  633. $('#script-content').insertAdjacentElement('afterbegin', notice);
  634. }
  635.  
  636. #tip(text='', timeout=0) {
  637. this.#elements.tip.innerText = text;
  638. timeout > 0 && setTimeout(() => this.#elements.tip.innerText = '', timeout);
  639. }
  640.  
  641. // Apply this.#sets to gui
  642. #displaySets() {
  643. // Save selected set
  644. const old_value = this.#elements.groups.value;
  645. [...this.#elements.groups.children].forEach(child => child.remove());
  646.  
  647. // Make <option>s
  648. this.#sets.forEach(set => {
  649. // Create <option>
  650. set.elmOption = $$CrE({
  651. tagName: 'option',
  652. props: {
  653. innerText: set.name,
  654. value: set.id
  655. }
  656. });
  657. // Display inset status
  658. if (set.scripts) {
  659. const inSet = set.scripts.includes(this.#sid);
  660. set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[inSet+0]} ${set.name}`;
  661. }
  662. // Append <option> into <select>
  663. this.#elements.groups.appendChild(set.elmOption);
  664. });
  665.  
  666. // Adjust <select> width
  667. this.#elements.groups.style.width = Math.max.apply(null, Array.from(this.#elements.groups.children).map(o => o.innerText.length)).toString() + 'em';
  668.  
  669. // Select previous selected set's <option>
  670. const selected = old_value ? [...this.#elements.groups.children].find(option => option.value === old_value) : null;
  671. selected && (selected.selected = true);
  672.  
  673. // Set edit-button.href
  674. const curset = this.#sets.find(set => set.id === this.#elements.groups.value);
  675. this.#elements.btnEdit.href = curset.linkedit;
  676.  
  677. // Display correct button
  678. this.#refreshButtonDisplay();
  679. }
  680.  
  681. // Display only add button when script in current set, otherwise remove button
  682. #refreshButtonDisplay() {
  683. const set = this.#getCurrentSet();
  684. if (!set?.scripts) { return null; }
  685. if (set.scripts.includes(this.#sid)) {
  686. this.#elements.btnAdd.style.setProperty('display', 'none');
  687. this.#elements.btnRemove.style.removeProperty('display');
  688. return true;
  689. } else {
  690. this.#elements.btnRemove.style.setProperty('display', 'none');
  691. this.#elements.btnAdd.style.removeProperty('display');
  692. return false;
  693. }
  694. }
  695.  
  696. // Returns null if no <option>s yet
  697. #getCurrentSet() {
  698. return this.#sets.find(set => set.id === this.#elements.groups.value) || null;
  699. }
  700.  
  701. #disable() {
  702. [
  703. this.#elements.groups,
  704. this.#elements.btnAdd, this.#elements.btnRemove,
  705. this.#elements.btnEdit, this.#elements.btnIframe,
  706. this.#elements.btnCopy, this.#elements.btnSync
  707. ].forEach(element => FavoritePanel.#disableElement(element));
  708. }
  709.  
  710. #enable() {
  711. [
  712. this.#elements.groups,
  713. this.#elements.btnAdd, this.#elements.btnRemove,
  714. this.#elements.btnEdit, this.#elements.btnIframe,
  715. this.#elements.btnCopy, this.#elements.btnSync
  716. ].forEach(element => FavoritePanel.#enableElement(element));
  717. }
  718.  
  719. static #disableElement(element) {
  720. element.style.filter = 'grayscale(1) brightness(0.95)';
  721. element.style.opacity = '0.25';
  722. element.style.pointerEvents = 'none';
  723. element.tabIndex = -1;
  724. }
  725.  
  726. static #enableElement(element) {
  727. element.style.removeProperty('filter');
  728. element.style.removeProperty('opacity');
  729. element.style.removeProperty('pointer-events');
  730. element.tabIndex = 0;
  731. }
  732.  
  733. static #deepClone(val) {
  734. if (typeof structuredClone === 'function') {
  735. return structuredClone(val);
  736. } else {
  737. return JSON.parse(JSON.stringify(val));
  738. }
  739. }
  740.  
  741. static #lightClone(val) {
  742. if (['string', 'number', 'boolean', 'undefined', 'bigint', 'symbol', 'function'].includes(val) || val === null) {
  743. return val;
  744. }
  745. if (Array.isArray(val)) {
  746. return val.slice();
  747. }
  748. if (typeof val === 'object') {
  749. return Object.fromEntries(Object.entries(val));
  750. }
  751. }
  752. }
  753.  
  754. const panel = new FavoritePanel(CM);
  755. }
  756.  
  757. // Basic functions
  758.  
  759. // Copy text to clipboard (needs to be called in an user event)
  760. function copyText(text) {
  761. // Create a new textarea for copying
  762. const newInput = document.createElement('textarea');
  763. document.body.appendChild(newInput);
  764. newInput.value = text;
  765. newInput.select();
  766. document.execCommand('copy');
  767. document.body.removeChild(newInput);
  768. }
  769.  
  770. // Check whether current page url matches FuncInfo.checker rule
  771. // This code is copy and modified from FunctionLoader.check
  772. function testChecker(checker) {
  773. if (!checker) {return true;}
  774. const values = Array.isArray(checker.value) ? checker.value : [checker.value]
  775. return values.some(value => {
  776. switch (checker.type) {
  777. case 'regurl': {
  778. return !!location.href.match(value);
  779. }
  780. case 'func': {
  781. try {
  782. return value();
  783. } catch (err) {
  784. DoLog(LogLevel.Error, CONST.Text.Loader.CheckerError);
  785. DoLog(LogLevel.Error, err);
  786. return false;
  787. }
  788. }
  789. case 'switch': {
  790. return value;
  791. }
  792. case 'starturl': {
  793. return location.href.startsWith(value);
  794. }
  795. case 'startpath': {
  796. return location.pathname.startsWith(value);
  797. }
  798. default: {
  799. DoLog(LogLevel.Error, CONST.Text.Loader.CheckerInvalid);
  800. return false;
  801. }
  802. }
  803. });
  804. }
  805.  
  806. // Load all function-objs provided in funcs asynchronously, and merge return values into one return obj
  807. // funcobj: {[checker], [detectDom], func}
  808. function loadFuncs(oFuncs) {
  809. const returnObj = {};
  810.  
  811. oFuncs.forEach(oFunc => {
  812. if (!oFunc.checker || testChecker(oFunc.checker)) {
  813. if (oFunc.detectDom) {
  814. detectDom(oFunc.detectDom, e => execute(oFunc));
  815. } else {
  816. setTimeout(e => execute(oFunc), 0);
  817. }
  818. }
  819. });
  820.  
  821. return returnObj;
  822.  
  823. function execute(oFunc) {
  824. setTimeout(e => {
  825. const rval = oFunc.func(returnObj) || {};
  826. copyProps(rval, returnObj);
  827. }, 0);
  828. }
  829. }
  830.  
  831. function randint(min, max) {
  832. return Math.floor(Math.random() * (max - min + 1)) + min;
  833. }
  834. })();