Greasy Fork is available in English.

Greasyfork 快捷编辑收藏

在GF脚本页直接编辑收藏集

Fra 08.02.2024. Se den seneste versjonen.

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