/* eslint-disable no-multi-spaces */
// ==UserScript==
// @name Greasyfork 快捷编辑收藏
// @name:zh-CN Greasyfork 快捷编辑收藏
// @name:zh-TW Greasyfork 快捷編輯收藏
// @name:en Greasyfork script-set-edit button
// @namespace Greasyfork-Favorite
// @version 0.1.2
// @description 在GF脚本页添加快速打开收藏集编辑页面功能
// @description:zh-CN 在GF脚本页添加快速打开收藏集编辑页面功能
// @description:zh-TW 在GF腳本頁添加快速打開收藏集編輯頁面功能
// @description:en Add open script-set-edit-page button in GF script page
// @author PY-DNG
// @license GPL-3
// @match http*://greatest.deepsurf.us/*
// @match http*://sleazyfork.org/*
// @icon https://api.iowen.cn/favicon/get.php?url=greatest.deepsurf.us
// @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'
},
'zh-TW': {
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":
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...
}
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] ? li.children[1].href : 'https://greatest.deepsurf.us/' + $('#language-selector-locale').value + '/users/' + $('#nav-user-info>.user-profile-link>a').href.match(/zh-CN\/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
});
}
// 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;
}
})();