/* jshint esversion: 8 */
// ==UserScript==
// @name Google & baidu Switcher (ALL in One)
// @name:en Google & baidu & Bing Switcher (ALL in One)
// @name:zh-TW 谷歌、百度、必應的搜索引擎跳轉工具
// @version 3.2.20210614.1
// @author F9y4ng
// @description 谷歌、百度、必应的搜索引擎跳转工具,脚本默认自动更新检测,可在菜单自定义设置必应按钮,搜索引擎跳转的最佳体验。
// @description:en Google, Baidu and Bing search engine tool, Automatically updated and detected by default, The Bing button can be customized.
// @description:zh-TW 谷歌、百度、必應的搜索引擎跳轉工具,腳本默認自動更新檢測,可在菜單自定義設置必應按鈕,搜索引擎跳轉的最佳體驗。
// @namespace https://openuserjs.org/scripts/t3xtf0rm4tgmail.com/Google_baidu_Switcher_(ALL_in_One)
// @supportURL https://github.com/F9y4ng/GreasyFork-Scripts/issues
// @icon https://www.google.com/favicon.ico
// @include *://*.google.*/search*
// @include *://*.google.*/webhp*
// @include *://www.baidu.com/*
// @include *://ipv6.baidu.com/*
// @include *://www1.baidu.com/*
// @include *://image.baidu.com/*
// @include *://*.bing.com/*
// @exclude *://*.google.*/sorry*
// @exclude *://*.google.*/url*
// @exclude *://www.baidu.com/link*
// @compatible Chrome 兼容TamperMonkey, ViolentMonkey
// @compatible Firefox 兼容Greasemonkey4.0+, TamperMonkey, ViolentMonkey
// @compatible Opera 兼容TamperMonkey, ViolentMonkey
// @compatible Safari 兼容Tampermonkey • Safari
// @note 重构Fetch with request timeout.\n重构GM.notification Function.\n重构NoticeJs.js及Css,修正错误。
// @grant GM_info
// @grant GM_registerMenuCommand
// @grant GM.registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_openInTab
// @grant GM_getValue
// @grant GM.getValue
// @grant GM_setValue
// @grant GM.setValue
// @grant GM_deleteValue
// @grant GM.deleteValue
// @license GPL-3.0-only
// @create 2015-10-07
// @copyright 2015-2021, F9y4ng
// @run-at document-start
// @downloadURL none
// ==/UserScript==
!(function () {
"use strict";
/* customize */
const isVersionDetection = true; // Set "false" to turn off the Version Detection forever.
const isdebug = false; // set "true" to debug scripts, May cause script response slower.
/* The following variable is used to define the expiration time of version detection.
* In order to reduce the query pressure on the script source server as much as possible,
* Please don`t set the Query-cache expiration time too short. So we set 4 hours by default,
* And to reduce update-tips frequency, you can extend the expireTime to a few days, or even weeks.
* (s = second, m = minute, h = hour, d = day, w = week) */
const expireTime = "4h";
/* Perfectly Compatible For Greasemonkey4.0+, TamperMonkey, ViolentMonkey * F9y4ng * 20210609 */
let GMsetValue, GMgetValue, GMdeleteValue, GMregisterMenuCommand, GMunregisterMenuCommand, GMnotification, GMopenInTab;
const GMinfo = GM_info;
const handlerInfo = GMinfo.scriptHandler;
const isGM = Boolean(handlerInfo.toLowerCase() === "greasemonkey");
const debug = isdebug ? console.log.bind(console) : () => {};
const error = isdebug ? console.error.bind(console) : () => {};
const defCon = {
scriptName: GMinfo.script.name,
curVersion: GMinfo.script.version,
support: GMinfo.script.supportURL,
encrypt: n => {
return window.btoa(encodeURIComponent(n));
},
decrypt: n => {
return decodeURIComponent(window.atob(n));
},
fetchResult: "success",
titleCase: (str, bool) => {
// bool: true for Whole sentence.
const RegExp = bool ? /( |^)[a-z]/g : /(^)[a-z]/g;
return str
.toString()
.toLowerCase()
.replace(RegExp, L => {
return L.toUpperCase();
});
},
isNoticed: sessionStorage.getItem("nCount") || 0,
isNeedUpdate: 0,
updateNote: "",
restTime: 0,
durationTime: t => {
let w, d, h, m, s;
const wks = Math.floor(t / 1000 / 60 / 60 / 24 / 7);
const ds = Math.floor(t / 1000 / 60 / 60 / 24 - wks * 7);
const hs = Math.floor(t / 1000 / 60 / 60 - wks * 7 * 24 - ds * 24);
const ms = Math.floor(t / 1000 / 60 - wks * 7 * 24 * 60 - ds * 24 * 60 - hs * 60);
const ss = Math.floor(t / 1000 - wks * 7 * 24 * 60 * 60 - ds * 24 * 60 * 60 - hs * 60 * 60 - ms * 60);
wks > 0 ? (w = ` ${wks}wk`) : (w = "");
ds > 0 ? (d = ` ${ds}d`) : (d = "");
hs > 0 ? (h = ` ${hs}h`) : (h = "");
ms > 0 ? (m = ` ${ms}min`) : (m = "");
wks > 0 || ds > 0 || hs > 0 || ms > 0 ? (s = "") : ss > 0 ? (s = ` ${ss}s`) : (s = " Destroying cache.");
return `${w}${d}${h}${m}${s}`;
},
randString: (n, v, r, s = "") => {
// v: true for only letters.
let a = "0123456789";
let b = "abcdefghijklmnopqrstuvwxyz";
let c = b.toUpperCase();
n = Number.isFinite(n) ? n : 10;
v ? (r = b + c) : (r = a + b + a + c);
for (; n > 0; --n) {
s += r[Math.floor(Math.random() * r.length)];
}
return s;
},
isUpgrade: Boolean(GetUrlParam("Zn")),
lastRuntime: new Date().toLocaleString("en-US", {
timeZoneName: "short",
hour12: false,
}),
};
defCon.rName = defCon.randString(7, true);
defCon.noticeHTML = str => {
return String(`
${str}
`);
};
if (isGM) {
GMsetValue = GM.setValue;
GMgetValue = GM.getValue;
GMdeleteValue = GM.deleteValue;
GMregisterMenuCommand = GM.registerMenuCommand;
GMunregisterMenuCommand = () => {};
GMopenInTab = (a, b) => {
window.open(a, defCon.randString(b ? b.length : 10).slice(-6));
};
} else {
GMsetValue = GM_setValue;
GMgetValue = GM_getValue;
GMdeleteValue = GM_deleteValue;
GMregisterMenuCommand = GM_registerMenuCommand;
GMunregisterMenuCommand = GM_unregisterMenuCommand;
GMopenInTab = GM_openInTab;
}
/* Refactoring functions of GMsetValue/GMgetValue/GMdeleteValue with Expire */
function GMsetExpire(key, value, expire) {
let obj = {
data: value,
time: Date.now(),
expire: /(?!^0)^[0-9]+[smhdw]$/i.test(expire) ? expire : "4h",
};
GMsetValue(key, JSON.stringify(obj));
}
function GMgetExpire(key, val) {
let expire, expires, expireTime;
if (!val) {
return val;
}
val = JSON.parse(val);
if (val.expire) {
/(?!^0)^[0-9]+[smhdw]$/i.test(val.expire) ? (expire = val.expire) : (expire = "4h");
expire = expire
.replace(/w/i, "*7*24*3600*1000")
.replace(/d/i, "*24*3600*1000")
.replace(/h/i, "*3600*1000")
.replace(/m/i, "*60*1000")
.replace(/s/i, "*1000");
expires = expire.split("*");
expireTime = expires.reduce(function (a, b) {
return a * b;
}, 1);
}
defCon.restTime = val.time + expireTime - Date.now();
if (defCon.restTime <= 0) {
GMdeleteValue(key);
return null;
}
return val.data;
}
/* Refactoring GMnotification Function */
GMnotification = (text = "", type = "info", closeWith = true, timeout = 30, { ...options } = {}) => {
try {
new NoticeJs({
text: text,
type: type,
closeWith: closeWith ? ["button"] : ["click"],
timeout: timeout,
callbacks: { ...options },
}).show();
} catch (e) {
error("//-> %cGMnotification:\n%c%s", "font-weight:bold", "font-weight:normal", e);
}
};
const callback_Countdown = {
onShow: [
function (Interval = 3) {
const m = setInterval(() => {
Interval ? --Interval : clearInterval(m);
const emText = document.querySelector(`.${defCon.rName} dl dd em`);
if (emText) {
emText.innerHTML = Interval;
}
}, 1e3);
},
],
};
/* Common functions */
function GetUrlParam(paraName) {
if (!paraName) {
const parameter = document.location.pathname.toString();
const arr = parameter.split("/");
return arr[1] === undefined ? "" : arr[1];
} else {
const url = document.location.toString();
const arrObj = url.split("?");
if (arrObj.length > 1) {
const arrPara = arrObj[1].split("&");
let arr;
for (let i = 0; i < arrPara.length; i++) {
arr = arrPara[i].split("=");
if (arr !== null && arr[0] === paraName) {
return arr[1];
}
}
return "";
} else {
return "";
}
}
}
function safeFunction(func) {
try {
func();
} catch (e) {
error("//-> %cFunctions:\n%c%s", "font-weight:bold", "font-weight:normal", e);
}
}
/* SYSTEM INFO */
console.info(
`%c[GB-Init]%c\nVersion: ${defCon.curVersion} %c[%s]%c\nExtension: %s\nlastRuntime: ${defCon.lastRuntime}`,
"font-weight:bold;color:dodgerblue",
"color:0",
"color:snow",
checkVersion(defCon.isUpgrade) instanceof Object === isVersionDetection,
"color:0",
defCon.titleCase(handlerInfo)
);
/* Version Detection with Cache and timeout * F9y4ng * 20210614 */
function fetchTimeout(url, time, { ...options } = {}) {
const controller = new AbortController();
const signal = controller.signal;
return new Promise((resolve, reject) => {
let t = setTimeout(() => {
controller.abort();
resolve(new Response("timeout", { status: 504, statusText: `Request timeout. (User-Defined: ${time}ms)` }));
}, time);
fetch(url, { signal: signal, ...options }).then(
res => {
clearTimeout(t);
resolve(res);
},
err => {
clearTimeout(t);
reject(err);
}
);
});
}
function fetchVersion(u) {
return new Promise((e, t) => {
fetchTimeout(u, 2000, {
method: "GET",
mode: "cors",
cache: "no-store",
credentials: "omit",
})
.then(e => {
debug("//-> %c%s %s", "color:green", e.ok, e.status);
if (!e.ok) {
throw Error(`${e.status} ${e.statusText}`);
}
return e.text();
})
.then(t => {
let n = defCon.curVersion;
let m = defCon.updateNote;
t.split(/[\r\n]+/).forEach(function (item) {
let key = item.match(/^(\/\/\s+@version\s+)(\S+)$/);
if (key) {
n = key[2];
}
let note = item.match(/^(\/\/\s+@note\s+)(.+)$/);
if (note) {
m = note[2];
}
});
e([compareVersion(defCon.curVersion, n), defCon.encrypt(n), defCon.encrypt(m), defCon.encrypt(u)]);
})
.catch(e => {
error("//-> %cfetchVersion:\n%c%s", "font-weight:bold", "font-weight:normal", e);
t();
});
});
}
function compareVersion(current, compare) {
let compare_array = compare.split(".");
let current_array = current.split(".");
let upgradeID = 0;
if (compare_array.length === current_array.length) {
for (let i = 0; i < compare_array.length; i++) {
if (parseInt(compare_array[i]) < parseInt(current_array[i])) {
upgradeID = 2;
break;
} else {
if (parseInt(compare_array[i]) === parseInt(current_array[i])) {
continue;
} else {
upgradeID = 1;
break;
}
}
}
} else {
upgradeID = 2;
}
return upgradeID;
}
async function checkVersion(s = false) {
let t, setResult, info;
const m = await GMgetValue("_is_Ver_Det_");
isVersionDetection ? (setResult = m === undefined ? isVersionDetection : Boolean(m)) : (setResult = false);
if (setResult) {
// load cache
const exp = await GMgetValue("_Check_Version_Expire_");
const cache = GMgetExpire("_Check_Version_Expire_", exp);
// Checking the local cache to reduce server requests
if (!cache) {
// first: greasyfork
t = await fetchVersion(`https://greasyfork.org/scripts/12909/code/${defCon.randString(32)}.meta.js`).catch(async () => {
defCon.fetchResult = "GreasyFork - Failed to fetch";
error(defCon.fetchResult);
});
// second: github
if (defCon.fetchResult.includes("GreasyFork")) {
t = await fetchVersion(`https://raw.githubusercontent.com/F9y4ng/GreasyFork-Scripts/master/Google%20%26%20Baidu%20Switcher.meta.js`).catch(
async () => {
defCon.fetchResult = "Github - Failed to fetch";
error(defCon.fetchResult);
}
);
}
// final: Jsdelivr points to gitee
if (defCon.fetchResult.includes("Github")) {
t = await fetchVersion(`https://cdn.jsdelivr.net/gh/F9y4ng/GreasyFork-Scripts@master/Google%20&%20Baidu%20Switcher.meta.js`).catch(
async () => {
defCon.fetchResult = "Jsdelivr - Failed to fetch";
error(defCon.fetchResult);
}
);
}
// Set value with expire
if (t !== undefined) {
GMsetExpire("_Check_Version_Expire_", t, expireTime);
debug("//-> checkVersion: Loading Data from Server.");
} else {
console.error(
"%c[GB-Update]\n%cSome unknown exceptions cause version detection failure, most likely by a network error. Please try again.",
"font-weight:bold;color:red",
"font-weight:bold;color:darkred"
);
}
} else {
t = cache;
debug("//-> checkVersion: Loading Data from Cache.");
}
// Resolution return data
if (typeof t !== "undefined") {
const lastestVersion = defCon.decrypt(t[1]);
defCon.isNeedUpdate = cache ? compareVersion(defCon.curVersion, lastestVersion) : t[0];
const updateNote = ((w = "") => {
if (defCon.decrypt(t[2])) {
defCon
.decrypt(t[2])
.split(/\\n/)
.forEach(function (item) {
w += `
${item}
`;
});
}
return w ? `
${w}
` : "";
})();
const updateUrl = defCon.decrypt(t[3]).replace("meta", "user");
const recheckURLs = new URL(
updateUrl
.replace("raw.githubusercontent", "github")
.replace("cdn.jsdelivr.net/gh", "gitee.com")
.replace("@", "/")
.replace("master", "blob/master")
.replace(/code\/[^/]+\.js/, "")
);
let sourceSite = defCon.titleCase(recheckURLs.hostname).split(".")[0];
sourceSite = cache ? `${sourceSite} on Cache` : sourceSite;
const repo = cache ? `\nCache expire:${defCon.durationTime(defCon.restTime)}\n` : `\n`;
switch (defCon.isNeedUpdate) {
case 2:
console.warn(
String(
`%c[GB-Update]%c\nWe found a new version, But %cthe latest version ` +
`%c${lastestVersion}%c is lower than your local version %c${defCon.curVersion}.%c\n\n` +
`Please confirm whether you need to upgrade your local script, and then you need to update it manually.\n\n` +
`If you no longer need the update prompt, please set "isVersionDetection" to "false" in your local code!\n` +
`${repo}(${sourceSite})`
),
"font-weight:bold;color:crimson",
"font-weight:bold;color:0",
"color:0",
"font-weight:bold;color:tomato",
"color:0",
"font-weight:bold;color:darkred",
"color:0"
);
if (defCon.isNoticed < 2 || s) {
setTimeout(function () {
GMnotification(
defCon.noticeHTML(
`