Greasyfork 快捷编辑收藏

在GF脚本页添加快速打开收藏集编辑页面功能

Od 10.11.2023.. Pogledajte najnovija verzija.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

/* eslint-disable no-multi-spaces */

// ==UserScript==
// @name               Greasyfork 快捷编辑收藏
// @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.1.9.2
// @description        在GF脚本页添加快速打开收藏集编辑页面功能
// @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
// @match              http*://*.greasyfork.org/*
// @match              http*://*.sleazyfork.org/*
// @require            https://greasyfork.org/scripts/456034-basic-functions-for-userscripts/code/script.js?version=1226884
// @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
// ==/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 */

(function __MAIN__() {
    'use strict';

	const CONST = {
		Text: {
			'zh-CN': {
				FavEdit: '收藏集:',
				Add: '加入此集',
				Remove: '移出此集',
				Edit: '手动编辑',
				CopySID: '复制脚本ID',
				Working: ['工作中...', '就快好了...'],
				InSetStatus: ['[ ]', '[✔]'],
				Error: {
					AlreadyExist: '脚本已经在此收藏集中了',
					NotExist: '脚本不在此收藏集中',
					Unknown: '未知错误'
				}
			},
			'zh-TW': {
				FavEdit: '收藏集:',
				Add: '加入此集',
				Remove: '移出此集',
				Edit: '手動編輯',
				CopySID: '複製腳本ID',
				Working: ['工作中...', '就快好了...'],
				InSetStatus: ['[ ]', '[✔]'],
				Error: {
					AlreadyExist: '腳本已經在此收藏集中了',
					NotExist: '腳本不在此收藏集中',
					Unknown: '未知錯誤'
				}
			},
			'en': {
				FavEdit: 'Add to/Remove from favorite list: ',
				Add: 'Add',
				Remove: 'Remove',
				Edit: 'Edit Manually',
				CopySID: 'Copy-Script-ID',
				Working: ['Working...', 'Just a moment...'],
				InSetStatus: ['[ ]', '[✔]'],
				Error: {
					AlreadyExist: 'Script is already in set',
					NotExist: 'Script is not in set yet',
					Unknown: 'Unknown Error'
				}
			},
			'default': {
				FavEdit: 'Add to/Remove from favorite list: ',
				Add: 'Add',
				Remove: 'Remove',
				Edit: 'Edit Manually',
				CopySID: 'Copy-Script-ID',
				Working: ['Working...', 'Just a moment...'],
				InSetStatus: ['[ ]', '[✔]'],
				Error: {
					AlreadyExist: 'Script is already in set',
					NotExist: 'Script is not in set yet',
					Unknown: 'Unknown Error'
				}
			},
		}
	}

	// Get i18n code
	let i18n = navigator.language;
	if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}

	main()
	function main() {
		const HOST = getHost();
		const API = getAPI();

		// Common actions
		commons();

		// API-based actions
		switch(API[1]) {
			case "scripts":
				API[2] && centerScript(API);
				break;
			default:
				DoLog('API is {}'.replace('{}', API));
		}
	}

	function centerScript(API) {
		switch(API[3]) {
			case undefined:
				pageScript();
				break;
			case 'code':
				pageCode();
				break;
			case 'feedback':
				pageFeedback();
				break;
		}
	}

	function commons() {
		// Your common actions here...
		GMXHRHook(5);
	}

	function pageScript() {
		addFavPanel();
	}

	function pageCode() {
		addFavPanel();
	}

	function pageFeedback() {
		addFavPanel();
	}

	function addFavPanel() {
		if (!getUserpage()) {return false;}
		GUI();

		function GUI() {
			// Get elements
			const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');
			const script_parent = script_after.parentElement;

			// My elements
			const script_favorite = $CrE('div');
			script_favorite.id = 'script-favorite';
			script_favorite.style.margin = '0.75em 0';
			script_favorite.innerHTML = CONST.Text[i18n].FavEdit;

			const favorite_groups = $CrE('select');
			favorite_groups.id = 'favorite-groups';

			const stored_sets = GM_getValue('script-sets', {sets: []}).sets;
			for (const set of stored_sets) {
				// Make <option>
				const option = $CrE('option');
				option.innerText = set.name;
				option.value = set.linkedit;
				$APD(favorite_groups, option);
			}
			adjustWidth();

			getScriptSets(function(sets) {
				clearChildnodes(favorite_groups);
				for (const set of sets) {
					// Make <option>
					const option = set.elmOption = $CrE('option');
					option.innerText = set.name;
					option.value = set.linkedit;
					$APD(favorite_groups, option);
				}
				adjustWidth();

				// Set edit-button.href
				favorite_edit.href = favorite_groups.value;

				// Check script in-set status
				getInSets(sets, getStrSID(), inSets => {
					sets.forEach(set => {
						const inSet = inSets.includes(set);
						set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[inSet+0]} ${set.name}`;
					});
					adjustWidth();
				});
			})
			favorite_groups.addEventListener('change', function(e) {
				favorite_edit.href = favorite_groups.value;
			});

			const favorite_add = $CrE('a');
			favorite_add.id = 'favorite-add';
			favorite_add.innerHTML = CONST.Text[i18n].Add;
			favorite_add.style.margin = favorite_add.style.margin = '0px 0.5em';
			favorite_add.href = 'javascript:void(0);'
			favorite_add.addEventListener('click', e => addFav());

			const favorite_remove = $CrE('a');
			favorite_remove.id = 'favorite-add';
			favorite_remove.innerHTML = CONST.Text[i18n].Remove;
			favorite_remove.style.margin = favorite_remove.style.margin = '0px 0.5em';
			favorite_remove.href = 'javascript:void(0);'
			favorite_remove.addEventListener('click', e => removeFav());

			const favorite_edit = $CrE('a');
			favorite_edit.id = 'favorite-edit';
			favorite_edit.innerHTML = CONST.Text[i18n].Edit;
			favorite_edit.style.margin = favorite_edit.style.margin = '0px 0.5em';
			favorite_edit.target = '_blank';

			const favorite_copy = $CrE('a');
			favorite_copy.id = 'favorite-copy';
			favorite_copy.href = 'javascript: void(0);';
			favorite_copy.innerHTML = CONST.Text[i18n].CopySID;
			favorite_copy.addEventListener('click', function() {
				copyText(getStrSID());
			});

			// Append to document
			$APD(script_favorite, favorite_groups);
			script_parent.insertBefore(script_favorite, script_after);
			[favorite_add, favorite_remove, favorite_edit, favorite_copy].forEach(button => $APD(script_favorite, button));

			function adjustWidth() {
				favorite_groups.style.width = Math.max.apply(null, Array.from(favorite_groups.children).map((o) => (o.innerText.length))).toString() + 'em';
				favorite_groups.style.maxWidth = '40vw';
			}

			function addFav() {
				const option = favorite_groups.selectedOptions[0];
				const set = GM_getValue('script-sets').sets.find(set => set.linkedit === option.value);
				const url = favorite_groups.value;

				displayNotice(CONST.Text[i18n].Working[0]);
				modifyFav(favorite_groups.value, oDom => {
					const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === getStrSID());
					if (existingInput) {
						displayNotice(CONST.Text[i18n].Error.AlreadyExist);
						option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;
						return false;
					}

					const input = $CrE('input');
					input.value = getStrSID();
					input.name = 'scripts-included[]';
					input.type = 'hidden';
					$APD($(oDom, '#script-set-scripts'), input);
					displayNotice(CONST.Text[i18n].Working[1]);
				}, oDom => {
					const status = $(oDom, 'p.notice');
					const status_text = status ? status.innerText : CONST.Text[i18n].Error.Unknown;
					displayNotice(status_text);
					option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;
				}, onerror);
			}

			function removeFav() {
				const option = favorite_groups.selectedOptions[0];
				const set = GM_getValue('script-sets').sets.find(set => set.linkedit === option.value);
				const url = favorite_groups.value;

				displayNotice(CONST.Text[i18n].Working[0]);
				modifyFav(favorite_groups.value, oDom => {
					const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === getStrSID());
					if (!existingInput) {
						displayNotice(CONST.Text[i18n].Error.NotExist);
						option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;
						return false;
					}

					existingInput.remove();
					displayNotice(CONST.Text[i18n].Working[1]);
				}, oDom => {
					const status = $(oDom, 'p.notice');
					const status_text = status ? status.innerText : CONST.Text[i18n].Error.Unknown;
					displayNotice(status_text);
					option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;
				}, onerror);
			}

			function modifyFav(url, editCallback, finishCallback, onerror) {
				getDocument(url, oDom => {
					if (editCallback(oDom) === false) {
						return false;
					}
					const form = $(oDom, '.change-script-set');
					const data = new FormData(form);
					data.append('save', '1');

					GM_xmlhttpRequest({
						method: 'POST',
						url: toAbsoluteURL(form.getAttribute('action')),
						data,
						responseType: 'blob',
						onload: response => parseDocument(response.response, oDom => finishCallback(oDom)),
						onerror
					});
					/*
					const xhr = new XMLHttpRequest();
					xhr.open('POST', toAbsoluteURL(form.getAttribute('action')));
					xhr.responseType = 'blob';
					xhr.onload = e => parseDocument(xhr.response, oDom => finishCallback(oDom));
					xhr.onerror = onerror;
					xhr.send(data);
					*/
				});
			}

			function onerror() {
				displayNotice(CONST.Text[i18n].Error.Unknown);
			}

			function 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);
			}
		}
	}

	function getScriptSets(callback, args=[]) {
		const userpage = getUserpage();
		getDocument(userpage, function(oDom) {
			/*
			const user_script_sets = oDom.querySelector('#user-script-sets');
			const script_sets = [];

			for (const li of user_script_sets.querySelectorAll('li')) {
				// Get fav info
				const name = li.childNodes[0].nodeValue.trimRight();
				const link = li.children[0].href;
				const linkedit = li.children[1] ? li.children[1].href : 'https://greasyfork.org/' + $('#language-selector-locale').value + '/users/' + $('#nav-user-info>.user-profile-link>a').href.match(/[a-zA-Z\-]+\/users\/([^\/]*)/)[1] + '/sets/' + li.children[0].href.match(/[\?&]set=(\d+)/)[1] + '/edit';

				// Append to script_sets
				script_sets.push({
					name: name,
					link: link,
					linkedit: linkedit
				});
			}
			*/
			const script_sets = Array.from($(oDom, 'ul#user-script-sets').children).map(li => ({
				name: li.children[0].innerText,
				link: li.children[0].href,
				linkedit: li.children[1].href
			}));

			// Save to GM_storage
			GM_setValue('script-sets', {
				sets: script_sets,
				time: (new Date()).getTime(),
				version: '0.2'
			});

			// callback
			callback.apply(null, [script_sets].concat(args));
		});
	}

	function getUserpage() {
		const a = $('#nav-user-info>.user-profile-link>a');
		return a ? a.href : null;
	}

	function getInSet(set, sid, callback) {
		sid = sid.toString();
		getDocument(set.linkedit, oDom => {
			const inSet = [...$(oDom, '#script-set-scripts').children].some(input => input.value === sid);
			callback(inSet);
		});
	}

	function getInSets(sets, sid, callback) {
		const AM = new AsyncManager();
		const inSets = [];
		for (const set of sets) {
			AM.add();
			getInSet(set, sid, inSet => {
				inSet && inSets.push(set);
				AM.finish();
			});
		}
		AM.onfinish = e => {
			callback(inSets);
		};
		AM.finishEvent = true;
	}

	function getStrSID(url=location.href) {
		const API = getAPI(url);
		const strSID = API[2].match(/\d+/)[0];
		return strSID;
	}

	function getSID(url=location.href) {
		return Number(getStrSID(url));
	}
	// Basic functions
	function $APD(a,b) {return a.appendChild(b);}

	// Remove all childnodes from an element
	function clearChildnodes(element) {
		const cns = []
		for (const cn of element.childNodes) {
			cns.push(cn);
		}
		for (const cn of cns) {
			element.removeChild(cn);
		}
	}

	// Download and parse a url page into a html document(dom).
    // when xhr onload: callback.apply([dom, args])
    function getDocument(url, callback, args=[]) {
        GM_xmlhttpRequest({
            method       : 'GET',
            url          : url,
            responseType : 'blob',
			onloadstart  : function() {
				DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\'');
			},
            onload       : function(response) {
                const htmlblob = response.response;
				parseDocument(htmlblob, callback, args);
            }
        })
    }

	function parseDocument(htmlblob, callback, args=[]) {
		const reader = new FileReader();
		reader.onload = function(e) {
			const htmlText = reader.result;
			const dom = new DOMParser().parseFromString(htmlText, 'text/html');
			args = [dom].concat(args);
			callback.apply(null, args);
			//callback(dom, htmlText);
		}
		reader.readAsText(htmlblob, document.characterSet);
	}

	// 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);
    }

	// get '/' splited API array from a url
	function getAPI(url=location.href) {
		return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
	}

	// get host part from a url(includes '^https://', '/$')
	function getHost(url=location.href) {
		const match = location.href.match(/https?:\/\/[^\/]+\//);
		return match ? match[0] : match;
	}

	function toAbsoluteURL(relativeURL, base=`${location.protocol}//${location.host}/`) {
		return new URL(relativeURL, base).href;
	}

	function randint(min, max) {
		return Math.floor(Math.random() * (max - min + 1)) + min;
	}
})();