Greasy Fork is available in English.

Greasyfork Set Edit+

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

Version au 13/07/2024. Voir la dernière version.


Installer ce script?
  1. /* eslint-disable no-multi-spaces *//* eslint-disable no-return-assign */// ==UserScript==// @name Greasyfork script-set-edit button// @name:zh-CN Greasyfork 快捷编辑收藏// @name:zh-TW Greasyfork 快捷編輯收藏// @name:en Greasyfork script-set-edit button// @name:en-US Greasyfork script-set-edit button// @name:fr Greasyfork Set Edit+// @namespace Greasyfork-Favorite// @version 0.2.8.4// @description Add / Remove script into / from script set directly in GF script info page// @description:zh-CN 在GF脚本页直接编辑收藏集// @description:zh-TW 在GF腳本頁直接編輯收藏集// @description:en Add / Remove script into / from script set directly in GF script info page// @description:en-US Add / Remove script into / from script set directly in GF script info page// @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// @author PY-DNG// @license GPL-3.0-or-later// @match http*://*.greasyfork.org/*// @match http*://*.sleazyfork.org/*// @match http*://greasyfork.org/*// @match http*://sleazyfork.org/*// @require https://update.greasyfork.org/scripts/456034/1348286/Basic%20Functions%20%28For%20userscripts%29.js// @require https://update.greasyfork.org/scripts/449583/1324274/ConfigManager.js// @require https://gfork.dahi.icu/scripts/460385-gm-web-hooks/code/script.js?version=1221394// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAbBJREFUOE+Vk7GKGlEUhr8pAiKKDlqpCDpLUCzWBxCENBa+hBsL9wHsLWxXG4tNtcGH0MIiWopY7JSGEUWsbESwUDMw4Z7siLsZDbnlPff/7n/+e67G38sA6sAXIPVWXgA/###gfinRPuhfCoXCw3Q65XA4eLBl6zvw1S2eAZqmvTqOc5/NZhkMBqRSKWzbvgYxgbwquoAX4MGyLHK5HIlEgtFo9C+IOFEAo1gsWsvlUmyPx2MymYxAhsMh6XT6lpM7BXjWdf1xNpuRz+fl8GQywTAMGo0G1WpVnJxOJ692vinADP###AaZz+cCOR6PmKZJPB4XUb/fp1wuewF+KoBCf1JVBVE5dDodms3mWdDtdqlUKl6AX+8ALmS9XgtM0/5kvNlspKX9fv8RIgBp4bISCoXo9XqsVitKpRK6rrPb7STQ7XZ7eVRaeAYerz14OBxGOfL7/eIgmUwKzHEcJZEQ1eha1wBqPxqNihufzyeQWCzmtiPPqJYM0jWIyiISibBYLAgEAtTrdVqt1nmQXN0rcH/LicqmVqvRbrdN27bfjbKru+nk7ZD3Z7q4+b++82/YPKIrXsKZ3AAAAABJRU5ErkJggg==// @grant GM_xmlhttpRequest// @grant GM_setValue// @grant GM_getValue// @grant GM_listValues// @grant GM_deleteValue// @grant GM_registerMenuCommand// @grant GM_unregisterMenuCommand// ==/UserScript==/* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager queueTask testChecker registerChecker loadFuncs *//* global GMXHRHook GMDLHook ConfigManager */const GFScriptSetAPI = (function() {const API = {async getScriptSets() {const userpage = API.getUserpage();const oDom = await API.getDocument(userpage);const list = Array.from($(oDom, 'ul#user-script-sets').children);const NoSets = list.length === 1 && list.every(li => li.children.length === 1);const script_sets = NoSets ? [] : Array.from($(oDom, 'ul#user-script-sets').children).filter(li => li.children.length === 2).map(li => {try {return {name: li.children[0].innerText,link: li.children[0].href,linkedit: li.children[1].href,id: getUrlArgv(li.children[0].href, 'set')}} catch(err) {DoLog(LogLevel.Error, [li, err, li.children.length, li.children[0]?.innerHTML, li.children[1]?.innerHTML], 'error');Err(err);}});return script_sets;},async getSetScripts(url) {return [...$All(await API.getDocument(url), '#script-set-scripts>input[name="scripts-included[]"]')].map(input => input.value);},/*** @typedef {Object} SetsDataAPI* @property {Response} resp - api fetch response object* @property {boolean} ok - resp.ok (resp.status >= 200 && resp.status <= 299)* @property {(Object|null)} data - api response json data, or null if not resp.ok*//*** @returns {SetsDataAPI}*/async getSetsData() {const userpage = API.getUserpage();const url = (userpage.endsWith('/') ? userpage : userpage + '/') + 'sets'const resp = await fetch(url, { credentials: 'same-origin' });if (resp.ok) {return {ok: true,resp,data: await resp.json()};} else {return {ok: false,resp,data: null};}},/*** @returns {(string|null)} the user's profile page url, from page top-right link <a>.href*/getUserpage() {const a = $('#nav-user-info>.user-profile-link>a');return a ? a.href : null;},/*** @returns {(string|null)} the user's id, in string format*/getUserID() {const userpage = API.getUserpage(); //https://greasyfork.org/zh-CN/users/667968-pyudngreturn userpage ? userpage.match(/\/users\/(\d+)(-[^\/]*\/*)?/)[1] : null;},// editCallback recieves:// true: edit doc load success// false: already in set// finishCallback recieves:// text: successfully added to set with text tip `text`// true: successfully loaded document but no text tip found// false: xhr erroraddFav(url, sid, editCallback, finishCallback) {API.modifyFav(url, oDom => {const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === sid);if (existingInput) {editCallback(false);return false;}const input = $CrE('input');input.value = sid;input.name = 'scripts-included[]';input.type = 'hidden';$(oDom, '#script-set-scripts').appendChild(input);editCallback(true);}, oDom => {const status = $(oDom, 'p.notice');const status_text = status ? status.innerText : true;finishCallback(status_text);}, err => finishCallback(false));},// editCallback recieves:// true: edit doc load success// false: already not in set// finishCallback recieves:// text: successfully removed from set with text tip `text`// true: successfully loaded document but no text tip found// false: xhr errorremoveFav(url, sid, editCallback, finishCallback) {API.modifyFav(url, oDom => {const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === sid);if (!existingInput) {editCallback(false);return false;}existingInput.remove();editCallback(true);}, oDom => {const status = $(oDom, 'p.notice');const status_text = status ? status.innerText : true;finishCallback(status_text);}, err => finishCallback(false));},async modifyFav(url, editCallback, finishCallback, onerror) {const oDom = await API.getDocument(url);if (editCallback(oDom) === false) { return false; }const form = $(oDom, '.change-script-set');const data = new FormData(form);data.append('save', '1');// Use XMLHttpRequest insteadof GM_xmlhttpRequest because there's unknown issue with GM_xmlhttpRequest// Use XMLHttpRequest insteadof GM_xmlhttpRequest before Tampermonkey 5.0.0 because of FormData posting issuesif (true || typeof GM_xmlhttpRequest !== 'function' || (GM_info.scriptHandler === 'Tampermonkey' && !API.GM_hasVersion('5.0'))) {const xhr = new XMLHttpRequest();xhr.open('POST', API.toAbsoluteURL(form.getAttribute('action')));xhr.responseType = 'blob';xhr.onload = async e => finishCallback(await API.parseDocument(xhr.response));xhr.onerror = onerror;xhr.send(data);} else {GM_xmlhttpRequest({method: 'POST',url: API.toAbsoluteURL(form.getAttribute('action')),data,responseType: 'blob',onload: async response => finishCallback(await API.parseDocument(response.response)),onerror});}},// Download and parse a url page into a html document(dom).// Returns a promise fulfills with domasync getDocument(url, retry=5) {try {const response = await fetch(url, {method: 'GET',cache: 'reload',});if (response.status === 200) {const blob = await response.blob();const oDom = await API.parseDocument(blob);return oDom;} else {throw new Error(`response.status is not 200 (${response.status})`);}} catch(err) {if (--retry > 0) {return API.getDocument(url, retry);} else {throw err;}}/*return new Promise((resolve, reject) => {GM_xmlhttpRequest({method : 'GET',url : url,responseType : 'blob',onload : function(response) {if (response.status === 200) {const htmlblob = response.response;API.parseDocument(htmlblob).then(resolve).catch(reject);} else {re(response);}},onerror: err => re(err)});function re(err) {DoLog(`Get document failed, retrying: (${retry}) ${url}`);--retry > 0 ? API.getDocument(url, retry).then(resolve).catch(reject) : reject(err);}});*/},// Returns a promise fulfills with domparseDocument(htmlblob) {return new Promise((resolve, reject) => {const reader = new FileReader();reader.onload = function(e) {const htmlText = reader.r###lt;const dom = new DOMParser().parseFromString(htmlText, 'text/html');resolve(dom);}reader.onerror = err => reject(err);reader.readAsText(htmlblob, document.characterSet);});},toAbsoluteURL(relativeURL, base=`${location.protocol}//${location.host}/`) {return new URL(relativeURL, base).href;},GM_hasVersion(version) {return hasVersion(GM_info?.version || '0', version);function hasVersion(ver1, ver2) {return compareVersions(ver1.toString(), ver2.toString()) >= 0;// https://greasyfork.org/app/javascript/versioncheck.js// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/formatfunction compareVersions(a, b) {if (a == b) {return 0;}let aParts = a.split('.');let bParts = b.split('.');for (let i = 0; i < aParts.length; i++) {let r###lt = compareVersionPart(aParts[i], bParts[i]);if (r###lt != 0) {return r###lt;}}// If all of a's parts are the same as b's parts, but b has additional parts, b is greater.if (bParts.length > aParts.length) {return -1;}return 0;}function compareVersionPart(partA, partB) {let partAParts = parseVersionPart(partA);let partBParts = parseVersionPart(partB);for (let i = 0; i < partAParts.length; i++) {// "A string-part that exists is always less than a string-part that doesn't exist"if (partAParts[i].length > 0 && partBParts[i].length == 0) {return -1;}if (partAParts[i].length == 0 && partBParts[i].length > 0) {return 1;}if (partAParts[i] > partBParts[i]) {return 1;}if (partAParts[i] < partBParts[i]) {return -1;}}return 0;}// It goes number, string, number, string. If it doesn't exist, then// 0 for numbers, empty string for strings.function parseVersionPart(part) {if (!part) {return [0, "", 0, ""];}let partParts = /([0-9]*)([^0-9]*)([0-9]*)([^0-9]*)/.exec(part)return [partParts[1] ? parseInt(partParts[1]) : 0,partParts[2],partParts[3] ? parseInt(partParts[3]) : 0,partParts[4]];}}}};return API;}) ();(function __MAIN__() {'use strict';const CONST = {Text: {'zh-CN': {FavEdit: '收藏集:',Add: '加入此集',Remove: '移出此集',Edit: '手动编辑',EditIframe: '页内编辑',CloseIframe: '关闭编辑',CopySID: '复制脚本ID',Sync: '同步',NotLoggedIn: '请先登录Greasyfork',NoSetsYet: '您还没有创建过收藏集',NewSet: '新建收藏集',Working: ['工作中...', '就快好了...'],InSetStatus: ['[ ]', '[✔]'],Groups: {Server: 'GreasyFork收藏集',Local: '本地收藏集',New: '新建'},Refreshing: {List: '获取收藏集列表...',Script: '获取收藏集内容...',Data: '获取收藏集数据...'},UseAPI: ['[ ] 使用GF的收藏集API', '[✔]使用GF的收藏集API'],Error: {AlreadyExist: '脚本已经在此收藏集中了',NotExist: '脚本不在此收藏集中',NetworkError: '网络错误',Unknown: '未知错误'}},'zh-TW': {FavEdit: '收藏集:',Add: '加入此集',Remove: '移出此集',Edit: '手動編輯',EditIframe: '頁內編輯',CloseIframe: '關閉編輯',CopySID: '複製腳本ID',Sync: '同步',NotLoggedIn: '請先登錄Greasyfork',NoSetsYet: '您還沒有創建過收藏集',NewSet: '新建收藏集',Working: ['工作中...', '就快好了...'],InSetStatus: ['[ ]', '[✔]'],Groups: {Server: 'GreasyFork收藏集',Local: '本地收藏集',New: '新建'},Refreshing: {List: '獲取收藏集清單...',Script: '獲取收藏集內容...',Data: '獲取收藏集數據...'},UseAPI: ['[ ] 使用GF的收藏集API', '[✔]使用GF的收藏集API'],Error: {AlreadyExist: '腳本已經在此收藏集中了',NotExist: '腳本不在此收藏集中',NetworkError: '網絡錯誤',Unknown: '未知錯誤'}},'en': {FavEdit: 'Script set: ',Add: 'Add',Remove: 'Remove',Edit: 'Edit Manually',EditIframe: 'In-Page Edit',CloseIframe: 'Close Editor',CopySID: 'Copy Script-ID',Sync: 'Sync',NotLoggedIn: 'Login to greasyfork to use script sets',NoSetsYet: 'You haven\'t created a collection yet',NewSet: 'Create a new set',Working: ['Working...', 'Just a moment...'],InSetStatus: ['[ ]', '[✔]'],Groups: {Server: 'GreasyFork',Local: 'Local',New: 'New'},Refreshing: {List: 'Fetching script sets...',Script: 'Fetching set content...',Data: 'Fetching script sets data...'},UseAPI: ['[ ] Use GF API', '[✔] Use GF API'],Error: {AlreadyExist: 'Script is already in set',NotExist: 'Script is not in set yet',NetworkError: 'Network Error',Unknown: 'Unknown Error'}},'default': {FavEdit: 'Script set: ',Add: 'Add',Remove: 'Remove',Edit: 'Edit Manually',EditIframe: 'In-Page Edit',CloseIframe: 'Close Editor',CopySID: 'Copy Script-ID',Sync: 'Sync',NotLoggedIn: 'Login to greasyfork to use script sets',NoSetsYet: 'You haven\'t created a collection yet',NewSet: 'Create a new set',Working: ['Working...', 'Just a moment...'],InSetStatus: ['[ ]', '[✔]'],Groups: {Server: 'GreasyFork',Local: 'Local',New: 'New'},Refreshing: {List: 'Fetching script sets...',Script: 'Fetching set content...',Data: 'Fetching script sets data...'},UseAPI: ['[ ] Use GF API', '[✔] Use GF API'],Error: {AlreadyExist: 'Script is already in set',NotExist: 'Script is not in set yet',NetworkError: 'Network Error',Unknown: 'Unknown Error'}},},URL: {SetLink: 'https://greasyfork.org/scripts?set=$ID',SetEdit: 'https://greasyfork.org/users/$UID/sets/$ID/edit'},ConfigRule: {'version-key': 'config-version',ignores: ['useAPI'],defaultValues: {'script-sets': {sets: [],time: 0,'config-version': 2,},'useAPI': true},'updaters': {/*'config-key': [function() {// This function contains updater for config['config-key'] from v0 to v1},function() {// This function contains updater for config['config-key'] from v1 to v2}]*/'script-sets': [config => {// v0 ==> v1// Fill set.idconst sets = config.sets;sets.forEach(set => {const id = getUrlArgv(set.link, 'set');set.id = id;set.scripts = null; // After first refresh, it should be an array of SIDs:string});// Delete old version identifierdelete config.version;return config;},config => {// v1 ==> v2return config}]},}};// Get i18n codelet i18n = $('#language-selector-locale') ? $('#language-selector-locale').value : navigator.language;if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}const CM = new ConfigManager(CONST.ConfigRule);const CONFIG = CM.Config;CM.updateAllConfigs();CM.setDefaults();loadFuncs([{name: 'Hook GM_xmlhttpRequest',checker: {type: 'switch',value: true},func: () => GMXHRHook(5)}, {name: 'Favorite panel',checker: {type: 'func',value: () => {const path = location.pathname.split('/').filter(p=>p);const index = path.indexOf('scripts');return [0,1].includes(index) && [undefined, 'code', 'feedback'].includes(path[index+2])}},func: addFavPanel}, {name: 'api-doc switch',checker: {type: 'switch',value: true},func: e => {makeBooleanSettings([{text: CONST.Text[i18n].UseAPI,key: 'useAPI',defaultValue: true}]);}}]);function addFavPanel() {//if (!GFScriptSetAPI.getUserpage()) {return false;}class FavoritePanel {#CM;#sid;#sets;#elements;#disabled;constructor(CM) {this.#CM = CM;this.#sid = location.pathname.match(/scripts\/(\d+)/)[1];this.#sets = this.#CM.getConfig('script-sets').sets;this.#elements = {};this.disabled = false;const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');const script_parent = script_after.parentElement;// Containerconst script_favorite = this.#elements.container = $$CrE({tagName: 'div',props: {id: 'script-favorite',innerHTML: CONST.Text[i18n].FavEdit},styles: { margin: '0.75em 0' }});// Selecterconst favorite_groups = this.#elements.select = $$CrE({tagName: 'select',props: { id: 'favorite-groups' },styles: { maxWidth: '40vw' },listeners: [['change', (() => {let lastSelected = 0;const record = () => lastSelected = favorite_groups.selectedIndex;const recover = () => favorite_groups.selectedIndex = lastSelected;return e => {const value = favorite_groups.value;const type = /^\d+$/.test(value) ? 'set-id' : 'command';switch (type) {case 'set-id': {const set = this.#sets.find(set => set.id === favorite_groups.value);favorite_edit.href = set.linkedit;break;}case 'command': {recover();this.#execCommand(value);}}this.#refreshButtonDisplay();record();}}) ()]]});favorite_groups.id = 'favorite-groups';// Buttonsconst makeBtn = (id, innerHTML, onClick, isLink=false) => $$CrE({tagName: 'a',props: {id, innerHTML,[isLink ? 'target' : 'href']: isLink ? '_blank' : 'javascript:void(0);'},styles: { margin: '0px 0.5em' },listeners: [['click', onClick]]});const favorite_add = this.#elements.btnAdd = makeBtn('favorite-add', CONST.Text[i18n].Add, e => this.#addFav());const favorite_remove = this.#elements.btnRemove = makeBtn('favorite-remove', CONST.Text[i18n].Remove, e => this.#removeFav());const favorite_edit = this.#elements.btnEdit = makeBtn('favorite-edit', CONST.Text[i18n].Edit, e => {}, true);const favorite_iframe = this.#elements.btnIframe = makeBtn('favorite-edit-in-page', CONST.Text[i18n].EditIframe, e => this.#editInPage(e));const favorite_copy = this.#elements.btnCopy = makeBtn('favorite-add', CONST.Text[i18n].CopySID, e => copyText(this.#sid));const favorite_sync = this.#elements.btnSync = makeBtn('favorite-sync', CONST.Text[i18n].Sync, e => this.#refresh());script_favorite.appendChild(favorite_groups);script_after.before(script_favorite);[favorite_add, favorite_remove, favorite_edit, favorite_iframe, favorite_copy, favorite_sync].forEach(button => script_favorite.appendChild(button));// Text tipconst tip = this.#elements.tip = $CrE('span');script_favorite.appendChild(tip);// Display cached sets firstthis.#displaySets();// Request GF document to update setsthis.#autoRefresh();}get sid() {return this.#sid;}get sets() {return FavoritePanel.#deepClone(this.#sets);}get elements() {return FavoritePanel.#lightClone(this.#elements);}#refresh() {const that = this;const method = CONFIG.useAPI ? 'api' : 'doc';return {api: () => this.#refresh_api(),doc: () => this.#refresh_doc()}[method]();}async #refresh_api() {const CONFIG = this.#CM.Config;this.#disable();this.#tip(CONST.Text[i18n].Refreshing.Data);// Check login statusif (!GFScriptSetAPI.getUserpage()) {this.#tip(CONST.Text[i18n].NotLoggedIn);return;}// Request sets data apiconst api_r###lt = await GFScriptSetAPI.getSetsData();const sets_data = api_r###lt.data;const uid = GFScriptSetAPI.getUserID();if (!api_r###lt.ok) {// When api fails, use doc as fallbackDoLog(LogLevel.Error, 'Sets API failed.');DoLog(LogLevel.Error, api_r###lt);return this.#refresh_doc();}// For forward compatibility, convert all setids and scriptids to string// and fill property set.link and set.linkeditfor (const set of sets_data) {// convert set id to stringset.id = set.id.toString();// https://greasyfork.org/zh-CN/scripts?set=439237set.link = replaceText(CONST.URL.SetLink, { $ID: set.id });// https://greasyfork.org/zh-CN/users/667968-pyudng/sets/439237/editset.linkedit = replaceText(CONST.URL.SetEdit, { $UID: uid, $ID: set.id });// there's two kind of sets: Favorite and non-favorite// favorite set's data is an array of object, where each object represents a script, with script's properties// non-favorite set's data is an array of ints, where each int means a script's id// For forward compatibility, we only store script ids, in string formatset.scripts.forEach((script, i, scripts) => {if (typeof script === 'number') {scripts[i] = script.toString();} else {scripts[i] = script.id.toString();}});}this.#sets = CONFIG['script-sets'].sets = sets_data;CONFIG['script-sets'].time = Date.now();this.#tip();this.#enable();this.#displaySets();this.#refreshButtonDisplay();}// Request document: get sets list andasync #refresh_doc() {const CONFIG = this.#CM.Config;this.#disable();this.#tip(CONST.Text[i18n].Refreshing.List);// Check login statusif (!GFScriptSetAPI.getUserpage()) {this.#tip(CONST.Text[i18n].NotLoggedIn);return;}// Refresh sets listthis.#sets = CONFIG['script-sets'].sets = await GFScriptSetAPI.getScriptSets();CONFIG['script-sets'].time = Date.now();this.#displaySets();// Refresh each set's script listthis.#tip(CONST.Text[i18n].Refreshing.Script);await Promise.all(this.#sets.map(async set => {// Fetch scriptsset.scripts = await GFScriptSetAPI.getSetScripts(set.linkedit);this.#displaySets();// Save to GM_storageconst setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);CONFIG['script-sets'].sets[setIndex].scripts = set.scripts;CONFIG['script-sets'].time = Date.now();}));this.#tip();this.#enable();this.#refreshButtonDisplay();}// Refresh on instance creation.// This should be running in low-frequecy. Refreshing makes lots of requests which may r###l in a 503 error(rate limit) for the user.#autoRefresh(minTime=1*24*60*60*1000) {const CONFIG = this.#CM.Config;const lastRefresh = new Date(CONFIG['script-sets'].time);if (Date.now() - lastRefresh > minTime) {this.#refresh();return true;} else {return false;}}#addFav() {const set = this.#getCurrentSet();const option = set.elmOption;this.#displayNotice(CONST.Text[i18n].Working[0]);let needRefresh = false;GFScriptSetAPI.addFav(this.#getCurrentSet().linkedit, this.#sid, editStatus => {if (!editStatus) {this.#displayNotice(CONST.Text[i18n].Error.AlreadyExist);option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;} else {this.#displayNotice(CONST.Text[i18n].Working[1]);}}, finishStatus => {if (finishStatus) {// Save to this.#sets and GM_storageif (needRefresh || CONFIG['script-sets'].sets.some(set => !set.scripts)) {// If scripts property is missing, do sync(refresh)this.#refresh();} else {const setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);CONFIG['script-sets'].sets[setIndex].scripts.push(this.#sid);this.#sets = CM.getConfig('script-sets').sets;}// Displaythis.#displayNotice(typeof finishStatus === 'string' ? finishStatus : CONST.Text[i18n].Error.Unknown);set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;this.#displaySets();} else {this.#displayNotice(CONST.Text[i18n].Error.NetworkError);}});}#removeFav() {const set = this.#getCurrentSet();const option = set.elmOption;this.#displayNotice(CONST.Text[i18n].Working[0]);let needRefresh = false;GFScriptSetAPI.removeFav(this.#getCurrentSet().linkedit, this.#sid, editStatus => {if (!editStatus) {this.#displayNotice(CONST.Text[i18n].Error.NotExist);option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;} else {this.#displayNotice(CONST.Text[i18n].Working[1]);}}, finishStatus => {if (finishStatus) {// Save to this.#sets and GM_storageif (needRefresh || CONFIG['script-sets'].sets.some(set => !set.scripts)) {// If scripts property is missing, do sync(refresh)this.#refresh();} else {const setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);const scriptIndex = CONFIG['script-sets'].sets[setIndex].scripts.indexOf(this.#sid);CONFIG['script-sets'].sets[setIndex].scripts.splice(scriptIndex, 1);this.#sets = CM.getConfig('script-sets').sets;}// Displaythis.#displayNotice(typeof finishStatus === 'string' ? finishStatus : CONST.Text[i18n].Error.Unknown);set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;this.#displaySets();} else {this.#displayNotice(CONST.Text[i18n].Error.NetworkError);}});}#editInPage(e) {e.preventDefault();const _iframes = [...$All(this.#elements.container, '.script-edit-page')];if (_iframes.length) {// Iframe exists, close iframethis.#elements.btnIframe.innerText = CONST.Text[i18n].EditIframe;_iframes.forEach(ifr => ifr.remove());this.#refresh();} else {// Iframe not exist, make iframethis.#elements.btnIframe.innerText = CONST.Text[i18n].CloseIframe;const iframe = $$CrE({tagName: 'iframe',props: {src: this.#getCurrentSet().linkedit},styles: {width: '100%',height: '60vh'},classes: ['script-edit-page'],listeners: [['load', e => {//this.#refresh();//iframe.style.height = iframe.contentDocument.body.parentElement.offsetHeight + 'px';}]]});this.#elements.container.appendChild(iframe);}}#displayNotice(text) {const notice = $CrE('p');notice.classList.add('notice');notice.id = 'fav-notice';notice.innerText = text;const old_notice = $('#fav-notice');old_notice && old_notice.parentElement.removeChild(old_notice);$('#script-content').insertAdjacentElement('afterbegin', notice);}#tip(text='', timeout=0) {this.#elements.tip.innerText = text;timeout > 0 && setTimeout(() => this.#elements.tip.innerText = '', timeout);}// Apply this.#sets to gui#displaySets() {const elements = this.#elements;// Save selected setconst old_value = elements.select.value;[...elements.select.children].forEach(child => child.remove());// Make <optgroup>s and <option>sconst serverGroup = elements.serverGroup = $$CrE({ tagName: 'optgroup', attrs: { label: CONST.Text[i18n].Groups.Server } });this.#sets.forEach(set => {// Create <option>set.elmOption = $$CrE({tagName: 'option',props: {innerText: set.name,value: set.id}});// Display inset statusif (set.scripts) {const inSet = set.scripts.includes(this.#sid);set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[inSet+0]} ${set.name}`;}// Append <option> into <select>serverGroup.appendChild(set.elmOption);});if (this.#sets.length === 0) {const optEmpty = elements.optEmpty = $$CrE({tagName: 'option',props: {innerText: CONST.Text[i18n].NoSetsYet,value: 'empty',selected: true}});serverGroup.appendChild(optEmpty);}const newGroup = elements.newGroup = $$CrE({ tagName: 'optgroup', attrs: { label: CONST.Text[i18n].Groups.New } });const newSet = elements.newSet = $$CrE({tagName: 'option',props: {innerText: CONST.Text[i18n].NewSet,value: 'new',}});newGroup.appendChild(newSet);[serverGroup, newGroup].forEach(optgroup => elements.select.appendChild(optgroup));// Adjust <select> widthelements.select.style.width = Math.max.apply(null, Array.from($All(elements.select, 'option')).map(o => o.innerText.length)).toString() + 'em';// Select previous selected set's <option>const selected = old_value ? [...$All(elements.select, 'option')].find(option => option.value === old_value) : null;selected && (selected.selected = true);// Set edit-button.hrefif (elements.select.value !== 'empty') {const curset = this.#sets.find(set => set.id === elements.select.value);elements.btnEdit.href = curset.linkedit;}// Display correct buttonthis.#refreshButtonDisplay();}// Display only add button when script in current set, otherwise remove button// Disable set-related buttons when not selecting options that not represents a set#refreshButtonDisplay() {const set = this.#getCurrentSet();!this.#disabled && ([this.#elements.btnAdd, this.#elements.btnRemove, this.#elements.btnEdit, this.#elements.btnIframe].forEach(element => set ? FavoritePanel.#enableElement(element) : FavoritePanel.#disableElement(element)));if (!set || !set.scripts) { return null; }if (set.scripts.includes(this.#sid)) {this.#elements.btnAdd.style.setProperty('display', 'none');this.#elements.btnRemove.style.removeProperty('display');return true;} else {this.#elements.btnRemove.style.setProperty('display', 'none');this.#elements.btnAdd.style.removeProperty('display');return false;}}#execCommand(command) {switch (command) {case 'new': {const url = GFScriptSetAPI.getUserpage() + (this.#getCurrentSet() ? '/sets/new' : '/sets/new?fav=1');window.open(url);break;}case 'empty': {// Do nothingbreak;}}}// Returns null if no <option>s yet#getCurrentSet() {return this.#sets.find(set => set.id === this.#elements.select.value) || null;}#disable() {[this.#elements.select,this.#elements.btnAdd, this.#elements.btnRemove,this.#elements.btnEdit, this.#elements.btnIframe,this.#elements.btnCopy, this.#elements.btnSync].forEach(element => FavoritePanel.#disableElement(element));this.#disabled = true;}#enable() {[this.#elements.select,this.#elements.btnAdd, this.#elements.btnRemove,this.#elements.btnEdit, this.#elements.btnIframe,this.#elements.btnCopy, this.#elements.btnSync].forEach(element => FavoritePanel.#enableElement(element));this.#disabled = false;}static #disableElement(element) {element.style.filter = 'grayscale(1) brightness(0.95)';element.style.opacity = '0.25';element.style.pointerEvents = 'none';element.tabIndex = -1;}static #enableElement(element) {element.style.removeProperty('filter');element.style.removeProperty('opacity');element.style.removeProperty('pointer-events');element.tabIndex = 0;}static #deepClone(val) {if (typeof structuredClone === 'function') {return structuredClone(val);} else {return JSON.parse(JSON.stringify(val));}}static #lightClone(val) {if (['string', 'number', 'boolean', 'undefined', 'bigint', 'symbol', 'function'].includes(val) || val === null) {return val;}if (Array.isArray(val)) {return val.slice();}if (typeof val === 'object') {return Object.fromEntries(Object.entries(val));}}}const panel = new FavoritePanel(CM);}// Basic functionsfunction makeBooleanSettings(settings) {for (const setting of settings) {makeBooleanMenu(setting.text, setting.key, setting.defaultValue, setting.callback, setting.initCallback);}function makeBooleanMenu(texts, key, defaultValue=false, callback=null, initCallback=false) {const initialVal = GM_getValue(key, defaultValue);const initialText = texts[initialVal + 0];let id = makeMenu(initialText, onClick);initCallback && callback(key, initialVal);function onClick() {const newValue = !GM_getValue(key, defaultValue);const newText = texts[newValue + 0];GM_setValue(key, newValue);id = makeMenu(newText, onClick, id);typeof callback === 'function' && callback(key, newValue);}function makeMenu(text, func, id) {if (GM_info.scriptHandler === 'Tampermonkey' && GM_hasVersion('5.0')) {return GM_registerMenuCommand(text, func, {id,autoClose: false,});} else {GM_unregisterMenuCommand(id);return GM_registerMenuCommand(text, func);}}}function GM_hasVersion(version) {return hasVersion(GM_info?.version || '0', version);function hasVersion(ver1, ver2) {return compareVersions(ver1.toString(), ver2.toString()) >= 0;// https://greasyfork.org/app/javascript/versioncheck.js// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/formatfunction compareVersions(a, b) {if (a == b) {return 0;}let aParts = a.split('.');let bParts = b.split('.');for (let i = 0; i < aParts.length; i++) {let r###lt = compareVersionPart(aParts[i], bParts[i]);if (r###lt != 0) {return r###lt;}}// If all of a's parts are the same as b's parts, but b has additional parts, b is greater.if (bParts.length > aParts.length) {return -1;}return 0;}function compareVersionPart(partA, partB) {let partAParts = parseVersionPart(partA);let partBParts = parseVersionPart(partB);for (let i = 0; i < partAParts.length; i++) {// "A string-part that exists is always less than a string-part that doesn't exist"if (partAParts[i].length > 0 && partBParts[i].length == 0) {return -1;}if (partAParts[i].length == 0 && partBParts[i].length > 0) {return 1;}if (partAParts[i] > partBParts[i]) {return 1;}if (partAParts[i] < partBParts[i]) {return -1;}}return 0;}// It goes number, string, number, string. If it doesn't exist, then// 0 for numbers, empty string for strings.function parseVersionPart(part) {if (!part) {return [0, "", 0, ""];}let partParts = /([0-9]*)([^0-9]*)([0-9]*)([^0-9]*)/.exec(part)return [partParts[1] ? parseInt(partParts[1]) : 0,partParts[2],partParts[3] ? parseInt(partParts[3]) : 0,partParts[4]];}}}}// Copy text to clipboard (needs to be called in an user event)function copyText(text) {// Create a new textarea for copyingconst newInput = document.createElement('textarea');document.body.appendChild(newInput);newInput.value = text;newInput.select();document.execCommand('copy');document.body.removeChild(newInput);}})();