/* 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.5
// @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*://*.gf.qytechs.cn/*
// @match http*://*.sleazyfork.org/*
// @match http*://gf.qytechs.cn/*
// @match http*://sleazyfork.org/*
// @require https://update.gf.qytechs.cn/scripts/456034/1303041/Basic%20Functions%20%28For%20userscripts%29.js
// @require https://update.gf.qytechs.cn/scripts/449583/1324274/ConfigManager.js
// @require https://gf.qytechs.cn/scripts/460385-gm-web-hooks/code/script.js?version=1221394
// @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==
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// ==/UserScript==
/* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */
/* 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);
},
getUserpage() {
const a = $('#nav-user-info>.user-profile-link>a');
return a ? a.href : 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 error
addFav(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 error
removeFav(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 issues
if (true || 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 dom
getDocument(url, retry=5) {
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 dom
parseDocument(htmlblob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(e) {
const htmlText = reader.result;
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://gf.qytechs.cn/app/javascript/versioncheck.js
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/format
function 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 result = compareVersionPart(aParts[i], bParts[i]);
if (result != 0) {
return result;
}
}
// 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: '获取收藏集内容...'
},
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: '獲取收藏集內容...'
},
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...'
},
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...'
},
Error: {
AlreadyExist: 'Script is already in set',
NotExist: 'Script is not in set yet',
NetworkError: 'Network Error',
Unknown: 'Unknown Error'
}
},
},
ConfigRule: {
'version-key': 'config-version',
ignores: [],
defaultValues: {
'script-sets': {
sets: [],
time: 0,
'config-version': 1,
},
},
'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 => {
// Fill set.id
const 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 identifier
delete config.version;
return config;
}
]
},
}
};
// Get i18n code
let 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();
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
}]);
function addFavPanel() {
//if (!GFScriptSetAPI.getUserpage()) {return false;}
class FavoritePanel {
#CM;
#sid;
#sets;
#elements;
constructor(CM) {
this.#CM = CM;
this.#sid = location.pathname.match(/scripts\/(\d+)/)[1];
this.#sets = this.#CM.getConfig('script-sets').sets;
this.#elements = {};
const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');
const script_parent = script_after.parentElement;
// Container
const script_favorite = this.#elements.container = $$CrE({
tagName: 'div',
props: {
id: 'script-favorite',
innerHTML: CONST.Text[i18n].FavEdit
},
styles: { margin: '0.75em 0' }
});
// Selecter
const 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';
// Buttons
const 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 tip
const tip = this.#elements.tip = $CrE('span');
script_favorite.appendChild(tip);
// Display cached sets first
this.#displaySets();
// Request GF document to update sets
this.#refresh();
}
get sid() {
return this.#sid;
}
get sets() {
return FavoritePanel.#deepClone(this.#sets);
}
get elements() {
return FavoritePanel.#lightClone(this.#elements);
}
// Request document: get sets list and
async #refresh() {
this.#disable();
this.#tip(CONST.Text[i18n].Refreshing.List);
// Check login status
if (!GFScriptSetAPI.getUserpage()) {
this.#tip(CONST.Text[i18n].NotLoggedIn);
return;
}
// Refresh sets list
this.#sets = CONFIG['script-sets'].sets = await GFScriptSetAPI.getScriptSets();
this.#displaySets();
// Refresh each set's script list
this.#tip(CONST.Text[i18n].Refreshing.Script);
await Promise.all(this.#sets.map(async set => {
// Fetch scripts
set.scripts = await GFScriptSetAPI.getSetScripts(set.linkedit);
this.#displaySets();
// Save to GM_storage
const setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);
CONFIG['script-sets'].sets[setIndex].scripts = set.scripts;
}));
this.#tip();
this.#enable();
this.#refreshButtonDisplay();
}
#addFav() {
const set = this.#getCurrentSet();
const option = set.elmOption;
this.#displayNotice(CONST.Text[i18n].Working[0]);
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_storage
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;
// Display
this.#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]);
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_storage
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;
// Display
this.#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 iframe
this.#elements.btnIframe.innerText = CONST.Text[i18n].EditIframe;
_iframes.forEach(ifr => ifr.remove());
this.#refresh();
} else {
// Iframe not exist, make iframe
this.#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 set
const old_value = elements.select.value;
[...elements.select.children].forEach(child => child.remove());
// Make <optgroup>s and <option>s
const 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 status
if (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> width
elements.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.href
if (elements.select.value !== 'empty') {
const curset = this.#sets.find(set => set.id === elements.select.value);
elements.btnEdit.href = curset.linkedit;
}
// Display correct button
this.#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();
if (!set) {
[this.#elements.btnAdd, this.#elements.btnRemove, this.#elements.btnEdit, this.#elements.btnIframe]
.forEach(element => FavoritePanel.#disableElement(element));
return null;
} else {
[this.#elements.btnAdd, this.#elements.btnRemove, this.#elements.btnEdit, this.#elements.btnIframe]
.forEach(element => FavoritePanel.#enableElement(element));
}
if (!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 nothing
break;
}
}
}
// 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));
}
#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));
}
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 functions
// Copy text to clipboard (needs to be called in an user event)
function copyText(text) {
// Create a new textarea for copying
const newInput = document.createElement('textarea');
document.body.appendChild(newInput);
newInput.value = text;
newInput.select();
document.execCommand('copy');
document.body.removeChild(newInput);
}
// Check whether current page url matches FuncInfo.checker rule
// This code is copy and modified from FunctionLoader.check
function testChecker(checker) {
if (!checker) {return true;}
const values = Array.isArray(checker.value) ? checker.value : [checker.value]
return values.some(value => {
switch (checker.type) {
case 'regurl': {
return !!location.href.match(value);
}
case 'func': {
try {
return value();
} catch (err) {
DoLog(LogLevel.Error, CONST.Text.Loader.CheckerError);
DoLog(LogLevel.Error, err);
return false;
}
}
case 'switch': {
return value;
}
case 'starturl': {
return location.href.startsWith(value);
}
case 'startpath': {
return location.pathname.startsWith(value);
}
default: {
DoLog(LogLevel.Error, CONST.Text.Loader.CheckerInvalid);
return false;
}
}
});
}
// Load all function-objs provided in funcs asynchronously, and merge return values into one return obj
// funcobj: {[checker], [detectDom], func}
function loadFuncs(oFuncs) {
const returnObj = {};
oFuncs.forEach(oFunc => {
if (!oFunc.checker || testChecker(oFunc.checker)) {
if (oFunc.detectDom) {
detectDom(oFunc.detectDom, e => execute(oFunc));
} else {
setTimeout(e => execute(oFunc), 0);
}
}
});
return returnObj;
function execute(oFunc) {
setTimeout(e => {
const rval = oFunc.func(returnObj) || {};
copyProps(rval, returnObj);
}, 0);
}
}
function randint(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
})();