- /* 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.9
- // @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://greasyfork.org/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
- // @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-pyudng
- return 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 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 || 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 dom
- async 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 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://greasyfork.org/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: '新建收藏集',
- sortByApiDefault: ['默认排序', '默认倒序'],
- 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: '新建收藏集',
- sortByApiDefault: ['默認排序', '默認倒序'],
- 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',
- sortByApiDefault: ['Default', 'Default reverse'],
- 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',
- sortByApiDefault: ['Default', 'Default reverse'],
- 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.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;
- },
- config => {
- // v1 ==> v2
- 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();
- 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).map(p => p.toLowerCase());
- const index = path.indexOf('scripts');
- const scripts_exist = [0,1].includes(index);
- const is_scripts_list = path.length-1 === index;
- const is_set_page = /[\?&]set=\d+/.test(location.search);
- const correct_page = [undefined, 'code', 'feedback'].includes(path[index+2]);
- return scripts_exist && !is_scripts_list && !is_set_page && correct_page;
- }
- },
- func: addFavPanel
- }, {
- name: 'api-doc switch',
- checker: {
- type: 'switch',
- value: true
- },
- func: e => {
- makeBooleanSettings([{
- text: CONST.Text[i18n].UseAPI,
- key: 'useAPI',
- defaultValue: true
- }]);
- }
- }, {
- name: 'Set scripts sort',
- checker: {
- type: 'func',
- value: () => {
- const scripts_exist = [1, 2].map(index => location.pathname.split('/')[index]?.toLowerCase()).includes('scripts');
- const is_set_page = /[\?&]set=\d+/.test(location.search);
- return scripts_exist && is_set_page;
- }
- },
- detectDom: '#script-list-sort>ul',
- func: e => {
- const search = new URLSearchParams(location.search);
- const set_id = search.get('set');
- const sort = search.get('sort');
- if (!CONFIG['script-sets'].sets.some(set => set.id === set_id)) { return false; }
-
- const ul = $('#script-list-sort>ul');
- [false, true].forEach(reverse => {
- const li = $$CrE({
- tagName: 'li',
- classes: ['list-option', 'gse-sort'], // gse: (G)resyfork(S)et(E)dit+
- attrs: { reverse: reverse ? '1' : '0' },
- });
- const a = $$CrE({
- tagName: 'a',
- props: { innerText: CONST.Text[i18n].sortByApiDefault[+reverse] },
- attrs: { rel: 'nofollow', href: getSortUrl(reverse) }
- });
- li.appendChild(a);
- ul.appendChild(li);
- });
- $AEL(ul, 'click', e => {
- if (e.target.matches('.gse-sort>a')) {
- e.preventDefault();
- const a = e.target;
- const li = a.parentElement;
- const reverse = !!+li.getAttribute('reverse');
- sortByApiDefault(reverse);
- buttonClicked(a);
- setSortUrl(reverse);
- }
- }, { capture: true });
-
- switch (sort) {
- case 'gse_default':
- sortByApiDefault(false);
- buttonClicked($('.gse-sort[reverse="0"]>a'));
- break;
- case 'gse_reverse':
- sortByApiDefault(true);
- buttonClicked($('.gse-sort[reverse="1"]>a'));
- break;
- }
-
- /**
- * Sort <li>s in #browse-script-list by default api order
- * Default api order is by add-to-set time right now (2024-07-21),
- * but this is not a promising feature
- */
- function sortByApiDefault(reverse=false) {
- const ol = $('#browse-script-list');
- const li_scripts = Array.from(ol.children);
- const set = CM.getConfig('script-sets').sets.find(set => set.id === set_id);
- const scripts = set.scripts;
- li_scripts.sort((li1, li2) => {
- const [sid1, sid2] = [li1, li2].map(li => li.getAttribute('data-script-id'));
- const [index1, index2] = [sid1, sid2].map(sid => scripts.indexOf(sid)).map(index => index >= 0 ? index : Infinity);
-
- return (reverse ? [1, -1] : [-1, 1])[index1 > index2 ? 1 : 0];
- });
- //li_scripts.forEach(li => ol.removeChild(li));
- li_scripts.forEach(li => ol.appendChild(li));
- }
-
- /**
- * Change the clicked button gui to given one
- */
- function buttonClicked(a) {
- const li = a.parentElement;
- const ul = li.parentElement;
- const old_li_current = Array.from(ul.children).find(li => li.classList.contains('list-current'));
-
- li.classList.add('list-current');
- a.remove();
- li.innerText = a.innerText;
-
- old_li_current.classList.remove('list-current');
- const old_li_a = $$CrE({
- tagName: 'a',
- attrs: { href: location.pathname + location.search + location.hash },
- props: { innerText: old_li_current.innerText }
- });
- old_li_current.innerText = '';
- old_li_current.appendChild(old_li_a);
- }
-
- /**
- * Set url search params when sorting
- */
- function setSortUrl(reverse) {
- history.replaceState({}, '', getSortUrl(reverse));
- }
-
- /**
- * Make corrent url search params with sorting
- */
- function getSortUrl(reverse) {
- const search = new URLSearchParams(location.search);
- search.set('sort', reverse ? 'gse_reverse' : 'gse_default');
- const url = location.pathname + '?' + search.toString();
- return url;
- }
- }
- }]);
-
- 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;
-
- // Sort sets by name in alphabetical order
- FavoritePanel.#sortSetsdata(this.#sets);
-
- 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.#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 status
- if (!GFScriptSetAPI.getUserpage()) {
- this.#tip(CONST.Text[i18n].NotLoggedIn);
- return;
- }
-
- // Request sets data api
- const api_result = await GFScriptSetAPI.getSetsData();
- const sets_data = api_result.data;
- const uid = GFScriptSetAPI.getUserID();
-
- if (!api_result.ok) {
- // When api fails, use doc as fallback
- DoLog(LogLevel.Error, 'Sets API failed.');
- DoLog(LogLevel.Error, api_result);
- return this.#refresh_doc();
- }
-
- // For forward compatibility, convert all setids and scriptids to string
- // and fill property set.link and set.linkedit
- for (const set of sets_data) {
- // convert set id to string
- set.id = set.id.toString();
- // https://greasyfork.org/zh-CN/scripts?set=439237
- set.link = replaceText(CONST.URL.SetLink, { $ID: set.id });
- // https://greasyfork.org/zh-CN/users/667968-pyudng/sets/439237/edit
- set.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 format
- set.scripts.forEach((script, i, scripts) => {
- if (typeof script === 'number') {
- scripts[i] = script.toString();
- } else {
- scripts[i] = script.id.toString();
- }
- });
- }
-
- // Sort sets by name in alphabetical order
- FavoritePanel.#sortSetsdata(sets_data);
-
- 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 and
- async #refresh_doc() {
- const CONFIG = this.#CM.Config;
-
- 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();
- CONFIG['script-sets'].time = Date.now();
- 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;
- 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 resul 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]);
- 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
- if (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;
- }
-
- // 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
- if (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;
- }
-
- // 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();
- !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 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));
- 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));
- }
- }
-
- static #sortSetsdata(sets_data) {
- // Sort sets by name in alphabetical order
- const sorted_names = sets_data.map(set => set.name).sort();
- if (sorted_names.includes('Favorite')) {
- // Keep set `Favorite` at first place
- sorted_names.splice(0, 0, sorted_names.splice(sorted_names.indexOf('Favorite'), 1)[0]);
- }
- sets_data.sort((setA, setB) => sorted_names.indexOf(setA.name) - sorted_names.indexOf(setB.name));
- }
- }
-
- const panel = new FavoritePanel(CM);
- }
-
- // Basic functions
- function 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/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]
- ];
- }
- }
- }
- }
-
- // 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);
- }
- })();