- /* eslint-disable no-multi-spaces */
-
- // ==UserScript==
- // @name Greasyfork 快捷编辑收藏
- // @name:zh-CN Greasyfork 快捷编辑收藏
- // @name:en Greasyfork script-set-edit button
- // @namespace Greasyfork-Favorite
- // @version 0.1
- // @description 在脚本信息页添加快速打开收藏集编辑页面功能
- // @description:zh-CN 在脚本信息页添加快速打开收藏集编辑页面功能
- // @description:en Add open script-set-edit-page button in script info page
- // @author PY-DNG
- // @license GPL-3
- // @match http*://greasyfork.org/*
- // @icon https://api.iowen.cn/favicon/get.php?url=greasyfork.org
- // @grant GM_xmlhttpRequest
- // @grant GM_setValue
- // @grant GM_getValue
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // Arguments: level=LogLevel.Info, logContent, asObject=false
- // Needs one call "DoLog();" to get it initialized before using it!
- function DoLog() {
- // Global log levels set
- window.LogLevel = {
- None: 0,
- Error: 1,
- Success: 2,
- Warning: 3,
- Info: 4,
- }
- window.LogLevelMap = {};
- window.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'}
- window.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'}
- window.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'}
- window.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'}
- window.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'}
- window.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}
-
- // Current log level
- DoLog.logLevel = (unsafeWindow ? unsafeWindow.isPY_DNG : window.isPY_DNG) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
-
- // Log counter
- DoLog.logCount === undefined && (DoLog.logCount = 0);
- if (++DoLog.logCount > 512) {
- console.clear();
- DoLog.logCount = 0;
- }
-
- // Get args
- let level, logContent, asObject;
- switch (arguments.length) {
- case 1:
- level = LogLevel.Info;
- logContent = arguments[0];
- asObject = false;
- break;
- case 2:
- level = arguments[0];
- logContent = arguments[1];
- asObject = false;
- break;
- case 3:
- level = arguments[0];
- logContent = arguments[1];
- asObject = arguments[2];
- break;
- default:
- level = LogLevel.Info;
- logContent = 'DoLog initialized.';
- asObject = false;
- break;
- }
-
- // Log when log level permits
- if (level <= DoLog.logLevel) {
- let msg = '%c' + LogLevelMap[level].prefix;
- let subst = LogLevelMap[level].color;
-
- if (asObject) {
- msg += ' %o';
- } else {
- switch(typeof(logContent)) {
- case 'string': msg += ' %s'; break;
- case 'number': msg += ' %d'; break;
- case 'object': msg += ' %o'; break;
- }
- }
-
- console.log(msg, subst, logContent);
- }
- }
- DoLog();
-
- bypassXB();
- GM_PolyFill('default');
-
- // Inner consts with i18n
- const CONST = {
- Text: {
- 'zh-CN': {
- FavEdit: '收藏集:',
- Edit: '编辑',
- CopySID: '复制脚本ID'
- },
- 'en': {
- FavEdit: 'Add to/Remove from favorite list: ',
- Edit: 'Edit',
- CopySID: 'Copy-Script-ID'
- },
- 'default': {
- FavEdit: 'Add to/Remove from favorite list: ',
- Edit: 'Edit',
- CopySID: 'Copy-Script-ID'
- },
- }
- }
-
- // 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":
- 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...
- }
-
- 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 = $C('div');
- script_favorite.id = 'script-favorite';
- script_favorite.style.margin = '0.75em 0';
- script_favorite.innerHTML = CONST.Text[i18n].FavEdit;
-
- const favorite_groups = $C('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 = $C('option');
- option.innerText = set.name;
- option.value = set.linkedit;
- $A(favorite_groups, option);
- }
-
- getScriptSets(function(sets) {
- clearChildnodes(favorite_groups);
- for (const set of sets) {
- // Make <option>
- const option = $C('option');
- option.innerText = set.name;
- option.value = set.linkedit;
- $A(favorite_groups, option);
- }
-
- // Set edit-button.href
- favorite_edit.href = favorite_groups.value;
- })
- favorite_groups.addEventListener('change', function(e) {
- favorite_edit.href = favorite_groups.value;
- });
-
- const favorite_edit = $C('a');
- favorite_edit.id = 'favorite-add';
- 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 = $C('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
- $A(script_favorite, favorite_groups);
- $I(script_parent, script_favorite, script_after);
- $A(script_favorite, favorite_edit);
- $A(script_favorite, favorite_copy);
- }
- }
-
- 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].href;
-
- // Append to script_sets
- script_sets.push({
- name: name,
- link: link,
- linkedit: linkedit
- });
- }
-
- // Save to GM_storage
- GM_setValue('script-sets', {
- sets: script_sets,
- time: (new Date()).getTime(),
- version: '0.1'
- });
-
- // 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 getStrSID(url=location.href) {
- const API = getAPI(url);
- const strSID = API[2].match(/\d+/);
- return strSID;
- }
-
- function getSID(url=location.href) {
- return Number(getStrSID(url));
- }
-
- function $(e) {return document.querySelector(e);}
- function $C(e) {return document.createElement(e);}
- function $A(a,b) {return a.appendChild(b);}
- function $I(a,b,c) {return a.insertBefore(b,c);}
-
- // 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);
- }
- }
-
- // Just stopPropagation and preventDefault
- function destroyEvent(e) {
- if (!e) {return false;};
- if (!e instanceof Event) {return false;};
- e.stopPropagation();
- e.preventDefault();
- }
-
- // 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);
- }
-
- // GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
- // Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
- // (If the request is invalid, such as url === '', will return false and will NOT make this request)
- // If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
- // Requires: function delItem(){...} & function uniqueIDMaker(){...}
- function GMXHRHook(maxXHR=5) {
- const GM_XHR = GM_xmlhttpRequest;
- const getID = uniqueIDMaker();
- let todoList = [], ongoingList = [];
- GM_xmlhttpRequest = safeGMxhr;
-
- function safeGMxhr() {
- // Get an id for this request, arrange a request object for it.
- const id = getID();
- const request = {id: id, args: arguments, aborter: null};
-
- // Deal onload function first
- dealEndingEvents(request);
-
- /* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
- // Stop invalid requests
- if (!validCheck(request)) {
- return false;
- }
- */
-
- // Judge if we could start the request now or later?
- todoList.push(request);
- checkXHR();
- return makeAbortFunc(id);
-
- // Decrease activeXHRCount while GM_XHR onload;
- function dealEndingEvents(request) {
- const e = request.args[0];
-
- // onload event
- const oriOnload = e.onload;
- e.onload = function() {
- reqFinish(request.id);
- checkXHR();
- oriOnload ? oriOnload.apply(null, arguments) : function() {};
- }
-
- // onerror event
- const oriOnerror = e.onerror;
- e.onerror = function() {
- reqFinish(request.id);
- checkXHR();
- oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
- }
-
- // ontimeout event
- const oriOntimeout = e.ontimeout;
- e.ontimeout = function() {
- reqFinish(request.id);
- checkXHR();
- oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
- }
-
- // onabort event
- const oriOnabort = e.onabort;
- e.onabort = function() {
- reqFinish(request.id);
- checkXHR();
- oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
- }
- }
-
- // Check if the request is invalid
- function validCheck(request) {
- const e = request.args[0];
-
- if (!e.url) {
- return false;
- }
-
- return true;
- }
-
- // Call a XHR from todoList and push the request object to ongoingList if called
- function checkXHR() {
- if (ongoingList.length >= maxXHR) {return false;};
- if (todoList.length === 0) {return false;};
- const req = todoList.shift();
- const reqArgs = req.args;
- const aborter = GM_XHR.apply(null, reqArgs);
- req.aborter = aborter;
- ongoingList.push(req);
- return req;
- }
-
- // Make a function that aborts a certain request
- function makeAbortFunc(id) {
- return function() {
- let i;
-
- // Check if the request haven't been called
- for (i = 0; i < todoList.length; i++) {
- const req = todoList[i];
- if (req.id === id) {
- // found this request: haven't been called
- delItem(todoList, i);
- return true;
- }
- }
-
- // Check if the request is running now
- for (i = 0; i < ongoingList.length; i++) {
- const req = todoList[i];
- if (req.id === id) {
- // found this request: running now
- req.aborter();
- reqFinish(id);
- checkXHR();
- }
- }
-
- // Oh no, this request is already finished...
- return false;
- }
- }
-
- // Remove a certain request from ongoingList
- function reqFinish(id) {
- let i;
- for (i = 0; i < ongoingList.length; i++) {
- const req = ongoingList[i];
- if (req.id === id) {
- ongoingList = delItem(ongoingList, i);
- return true;
- }
- }
- return false;
- }
- }
- }
-
- // Get a url argument from lacation.href
- // also recieve a function to deal the matched string
- // returns defaultValue if name not found
- // Args: name, dealFunc=(function(a) {return a;}), defaultValue=null
- function getUrlArgv(details) {
- typeof(details) === 'string' && (details = {name: details});
- typeof(details) === 'undefined' && (details = {});
- if (!details.name) {return null;};
-
- const url = details.url ? details.url : location.href;
- const name = details.name ? details.name : '';
- const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;});
- const defaultValue = details.defaultValue ? details.defaultValue : null;
- const matcher = new RegExp(name + '=([^&]+)');
- const result = url.match(matcher);
- const argv = result ? dealFunc(result[1]) : defaultValue;
-
- return argv;
- }
-
- // 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);
- }
-
- // Append a style text to document(<head>) with a <style> element
- function addStyle(css, id) {
- const style = document.createElement("style");
- id && (style.id = id);
- style.textContent = css;
- for (const elm of document.querySelectorAll('#'+id)) {
- elm.parentElement && elm.parentElement.removeChild(elm);
- }
- document.head.appendChild(style);
- }
-
- // File download function
- // details looks like the detail of GM_xmlhttpRequest
- // onload function will be called after file saved to disk
- function downloadFile(details) {
- if (!details.url || !details.name) {return false;};
-
- // Configure request object
- const requestObj = {
- url: details.url,
- responseType: 'blob',
- onload: function(e) {
- // Save file
- saveFile(URL.createObjectURL(e.response), details.name);
-
- // onload callback
- details.onload ? details.onload(e) : function() {};
- }
- }
- if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;};
- if (details.onprogress ) {requestObj.onprogress = details.onprogress;};
- if (details.onerror ) {requestObj.onerror = details.onerror;};
- if (details.onabort ) {requestObj.onabort = details.onabort;};
- if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
- if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;};
-
- // Send request
- GM_xmlhttpRequest(requestObj);
- }
-
- // 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;
- }
-
- // Your code here...
- // Bypass xbrowser's useless GM_functions
- function bypassXB() {
- if (typeof(mbrowser) === 'object') {
- window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined;
- }
- }
-
- // GM_Polyfill By PY-DNG
- // 2021.07.18 - 2021.07.19
- // Simply provides the following GM_functions using localStorage, XMLHttpRequest and window.open:
- // Returns object GM_POLYFILLED which has the following properties that shows you which GM_functions are actually polyfilled:
- // GM_setValue, GM_getValue, GM_deleteValue, GM_listValues, GM_xmlhttpRequest, GM_openInTab, GM_setClipboard, unsafeWindow(object)
- // All polyfilled GM_functions are accessable in window object/Global_Scope(only without Tempermonkey Sandboxing environment)
- function GM_PolyFill(name='default') {
- const GM_POLYFILL_KEY_STORAGE = 'GM_STORAGE_POLYFILL';
- let GM_POLYFILL_storage;
- const GM_POLYFILLED = {
- GM_setValue: true,
- GM_getValue: true,
- GM_deleteValue: true,
- GM_listValues: true,
- GM_xmlhttpRequest: true,
- GM_openInTab: true,
- GM_setClipboard: true,
- unsafeWindow: true,
- once: false
- }
-
- // Ignore GM_PolyFill_Once
- window.GM_POLYFILLED && window.GM_POLYFILLED.once && (window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined);
-
- GM_setValue_polyfill();
- GM_getValue_polyfill();
- GM_deleteValue_polyfill();
- GM_listValues_polyfill();
- GM_xmlhttpRequest_polyfill();
- GM_openInTab_polyfill();
- GM_setClipboard_polyfill();
- unsafeWindow_polyfill();
-
- function GM_POLYFILL_getStorage() {
- let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
- gstorage = gstorage ? JSON.parse(gstorage) : {};
- let storage = gstorage[name] ? gstorage[name] : {};
- return storage;
- }
-
- function GM_POLYFILL_saveStorage() {
- let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
- gstorage = gstorage ? JSON.parse(gstorage) : {};
- gstorage[name] = GM_POLYFILL_storage;
- localStorage.setItem(GM_POLYFILL_KEY_STORAGE, JSON.stringify(gstorage));
- }
-
- // GM_setValue
- function GM_setValue_polyfill() {
- typeof (GM_setValue) === 'function' ? GM_POLYFILLED.GM_setValue = false: window.GM_setValue = PF_GM_setValue;;
-
- function PF_GM_setValue(name, value) {
- GM_POLYFILL_storage = GM_POLYFILL_getStorage();
- name = String(name);
- GM_POLYFILL_storage[name] = value;
- GM_POLYFILL_saveStorage();
- }
- }
-
- // GM_getValue
- function GM_getValue_polyfill() {
- typeof (GM_getValue) === 'function' ? GM_POLYFILLED.GM_getValue = false: window.GM_getValue = PF_GM_getValue;
-
- function PF_GM_getValue(name, defaultValue) {
- GM_POLYFILL_storage = GM_POLYFILL_getStorage();
- name = String(name);
- if (GM_POLYFILL_storage.hasOwnProperty(name)) {
- return GM_POLYFILL_storage[name];
- } else {
- return defaultValue;
- }
- }
- }
-
- // GM_deleteValue
- function GM_deleteValue_polyfill() {
- typeof (GM_deleteValue) === 'function' ? GM_POLYFILLED.GM_deleteValue = false: window.GM_deleteValue = PF_GM_deleteValue;
-
- function PF_GM_deleteValue(name) {
- GM_POLYFILL_storage = GM_POLYFILL_getStorage();
- name = String(name);
- if (GM_POLYFILL_storage.hasOwnProperty(name)) {
- delete GM_POLYFILL_storage[name];
- GM_POLYFILL_saveStorage();
- }
- }
- }
-
- // GM_listValues
- function GM_listValues_polyfill() {
- typeof (GM_listValues) === 'function' ? GM_POLYFILLED.GM_listValues = false: window.GM_listValues = PF_GM_listValues;
-
- function PF_GM_listValues() {
- GM_POLYFILL_storage = GM_POLYFILL_getStorage();
- return Object.keys(GM_POLYFILL_storage);
- }
- }
-
- // unsafeWindow
- function unsafeWindow_polyfill() {
- typeof (unsafeWindow) === 'object' ? GM_POLYFILLED.unsafeWindow = false: window.unsafeWindow = window;
- }
-
- // GM_xmlhttpRequest
- // not supported properties of details: synchronous binary nocache revalidate context fetch
- // not supported properties of response(onload arguments[0]): finalUrl
- // ---!IMPORTANT!--- DOES NOT SUPPORT CROSS-ORIGIN REQUESTS!!!!! ---!IMPORTANT!---
- function GM_xmlhttpRequest_polyfill() {
- typeof (GM_xmlhttpRequest) === 'function' ? GM_POLYFILLED.GM_xmlhttpRequest = false: window.GM_xmlhttpRequest = PF_GM_xmlhttpRequest;
-
- // details.synchronous is not supported as Tempermonkey
- function PF_GM_xmlhttpRequest(details) {
- const xhr = new XMLHttpRequest();
-
- // open request
- const openArgs = [details.method, details.url, true];
- if (details.user && details.password) {
- openArgs.push(details.user);
- openArgs.push(details.password);
- }
- xhr.open.apply(xhr, openArgs);
-
- // set headers
- if (details.headers) {
- for (const key of Object.keys(details.headers)) {
- xhr.setRequestHeader(key, details.headers[key]);
- }
- }
- details.cookie ? xhr.setRequestHeader('cookie', details.cookie) : function () {};
- details.anonymous ? xhr.setRequestHeader('cookie', '') : function () {};
-
- // properties
- xhr.timeout = details.timeout;
- xhr.responseType = details.responseType;
- details.overrideMimeType ? xhr.overrideMimeType(details.overrideMimeType) : function () {};
-
- // events
- xhr.onabort = details.onabort;
- xhr.onerror = details.onerror;
- xhr.onloadstart = details.onloadstart;
- xhr.onprogress = details.onprogress;
- xhr.onreadystatechange = details.onreadystatechange;
- xhr.ontimeout = details.ontimeout;
- xhr.onload = function (e) {
- const response = {
- readyState: xhr.readyState,
- status: xhr.status,
- statusText: xhr.statusText,
- responseHeaders: xhr.getAllResponseHeaders(),
- response: xhr.response
- };
- (details.responseType === '' || details.responseType === 'text') ? (response.responseText = xhr.responseText) : function () {};
- (details.responseType === '' || details.responseType === 'document') ? (response.responseXML = xhr.responseXML) : function () {};
- details.onload(response);
- }
-
- // send request
- details.data ? xhr.send(details.data) : xhr.send();
-
- return {
- abort: xhr.abort
- };
- }
- }
-
- // NOTE: options(arg2) is NOT SUPPORTED! if provided, then will just be skipped.
- function GM_openInTab_polyfill() {
- typeof (GM_openInTab) === 'function' ? GM_POLYFILLED.GM_openInTab = false: window.GM_openInTab = PF_GM_openInTab;
-
- function PF_GM_openInTab(url) {
- window.open(url);
- }
- }
-
- // NOTE: needs to be called in an event handler function, and info(arg2) is NOT SUPPORTED!
- function GM_setClipboard_polyfill() {
- typeof (GM_setClipboard) === 'function' ? GM_POLYFILLED.GM_setClipboard = false: window.GM_setClipboard = PF_GM_setClipboard;
-
- function PF_GM_setClipboard(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);
- }
- }
-
- return GM_POLYFILLED;
- }
-
- // Makes a function that returns a unique ID number each time
- function uniqueIDMaker() {
- let id = 0;
- return makeID;
- function makeID() {
- id++;
- return id;
- }
- }
-
- // Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
- function delItem(arr, delIndex) {
- arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1));
- return arr;
- }
- })();