您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
(我已经安装了用户样式管理器,让我安装!)
// ==UserScript==
// @name eTools QOL improvements
// @namespace Violentmonkey Scripts
// @match https://www.etools.ch/*
// @grant none
// @version 1.002
// @author Ryan Wilson
// @license AGPL-3.0-or-later
// @description ...
// @require https://openuserjs.org/src/libs/sizzle/GM_config.min.js
// @require https://update.gf.qytechs.cn/scripts/421384/1134973/GM_fetch.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM.getValue
// @grant GM.setValue
// @grant GM_xmlhttpRequest
// ==/UserScript==
// TODO: Achieve better safety
//
// TODO: Support mobile and support engine-specific search with these options
// Not here but in the CSS Stylish add an option to disable the highlight
// TODO: Add a proper userscript config with GM_config
// - @see https://github.com/sizzlemctwizzle/GM_config/wiki/
// TODO: Use proper JSDoc annotations on the config object for all the properties
const config = {
yandexImageSearchBtn: true,
/* TODO: Implement */
collapsableSidebar: true,
removeHighlight: true,
disableMobileLink: false,
/** Bring the "By Topic:" menu to the top of the right sidebar **/
bringByTopicToTop: true,
moveSearchStatusesToBottom: true,
removeAffiliateLink: true,
displayFullLink: true,
faviSources: false,
showFavi: true, // Fetches the favicon from the sites in a bare proxy
autofill: {
enabled: true,
service: "brave",
},
preview: {
enable: false,
// This would allow for any site to be iframed with the preview feature, bringing preview to any site, using aero
proxyPreview: true,
},
};
const usingMobile =
location.pathname === "/mobileSearch.do" ||
location.pathname === "/mobileSearchSubmit.do";
const usingDesktop =
location.pathname === "/search.do" ||
location.pathname === "/searchSubmit.do";
if (usingDesktop) {
if (config.yandexImageSearchBtn) {
const searchBar = document.getElementsByClassName("query")[0];
const searchBtn = document.getElementsByClassName("submit")[0];
const imgSearchBtn = document.createElement("input");
imgSearchBtn.type = "submit";
imgSearchBtn.value = "Image Search";
imgSearchBtn.className = "submit";
imgSearchBtn.style.marginLeft = "5px";
imgSearchBtn.addEventListener("click", (e) => {
e.preventDefault();
location.href =
"https://yandex.com/images/search?text=" + searchBar.value;
});
searchBtn.after(imgSearchBtn);
}
if (config.moveSearchStatusesToBottom) {
const results = document.getElementsByClassName("result")[0];
const resultsTable = results.parentNode;
const statuses = [
...document.getElementsByClassName("searchStatus"),
].filter((status) => resultsTable.contains(status));
// Search statuses on eTools are optional so they might not exist
if (statuses.length !== 0) {
statuses.at(-1).style.paddingBottom = "15px";
statuses.forEach((status) => {
resultsTable.appendChild(status);
});
}
}
if (config.disableMobileLink) {
[...document.getElementsByTagName("a")]
.find((link) => (link.href = "mobileSearch.do"))
.removeAttribute("href");
// Consider: Perhaps I should add "mobile version" to the sidebar instead and make the favicon just redirect to the real home page, not the mobile version of the site? It would be a more logical place to put the mobile version.
}
if (config.bringByTopicToTop) {
// const sidebarLeft = [...document.getElementsByTagName("td")][100];
const sourceBox = [...document.getElementsByClassName("boxTop")][2];
const resultsToolTitle = [...document.getElementsByTagName("h3")].find(
(title) => title.textContent === "Results Tool"
);
if (resultsToolTitle)
resultsToolTitle.insertAdjacentElement("afterend", sourceBox);
}
if (config.faviSources) {
const links = [...document.getElementsByTagName("a")];
const engineToFavi = new Map();
// TODO: Find HD sources for each favicon
// TODO: Finish adding all of the sources
engineToFavi.set(
"Brave",
"https://cdn.search.brave.com/serp/v2/_app/immutable/assets/favicon-32x32.B2iBzfXZ.png"
);
engineToFavi.set("DuckDuckGo", "https://duckduckgo.com/favicon.ico");
engineToFavi.set("Google", "https://www.google.com/favicon.ico");
engineToFavi.set("Mojeek", "https://www.mojeek.com/favicon.ico");
engineToFavi.set("Qwant", "https://www.qwant.com/favicon.ico");
engineToFavi.set(
"Wikipedia",
"https://www.wikipedia.org/static/favicon/wikipedia.ico"
);
engineToFavi.set(
"Yandex",
"https://yastatic.net/s3/home-static/_/nova/7f2537ce.png"
);
for (let [text, faviLink] of engineToFavi) {
const ddgs = links.filter((link) => link.innerText === text);
ddgs.forEach((ddg) => {
ddg.innerText = "";
const favi = document.createElement("img");
favi.src = faviLink;
favi.style.width = "20px";
favi.style.height = "20px";
// TODO: Keep the link to the search engine but append the current query to it
ddg.insertAdjacentElement("afterend", favi);
});
}
}
if (!config.preview.enable) {
const pipeText = " | ";
// FIXME: Doesn't work on Engine-specific search
[...document.getElementsByTagName("a")]
.filter((link) => link.textContent === "preview" && /^p\d/.test(link.id))
.forEach((previewLink) => {
const pipe = previewLink.previousSibling;
// Parity check
if (pipe.textContent === pipeText) {
pipe.remove();
}
previewLink.remove();
});
}
// TODO: Add the "proxy preview" feature
if (config.proxyPreview) {
/*
[...document.getElementsByClassName("attr")].forEach(attr => {
[...attr.childNodes].forEach(child => {
if (child.nodeType === Node.TEXT_NODE) {
const isPipe = child.textContent === pipeText;
const isSource = child.textContent === "Source: ";
if (config.faviSources && (isPipe || isSource))
...
}
});
})
*/
}
}
function removeAffiliateLink(linkElement) {
if (linkElement.href.startsWith(affiliateRedirect))
linkElement.href = decodeURIComponent(
linkElement.href.split(affiliateRedirect).pop()
);
}
function desktopHandleRecord(record) {
const children = [...record.children];
const titleElement = children.find((el) => el instanceof HTMLAnchorElement);
if (config.removeAffiliateLink) removeAffiliateLink(titleElement);
if (config.displayFullLink) {
const titleLink = titleElement.href;
if (titleLink) {
const urlDiv = children.find((child) => child.className === "attr");
const attrChildren = [...urlDiv.children];
const displayLink = attrChildren[0];
displayLink.innerText = titleLink;
}
}
}
function mobileHandlePElement(pElement) {
const children = [...pElement.children];
const linkElement = children.find(
(child) => child instanceof HTMLAnchorElement
);
if (config.removeAffiliateLink) removeAffiliateLink(linkElement);
const varElement = children.find((child) => child.tagName === "VAR");
varElement.innerText = linkElement.href;
}
const recordObserver = new MutationObserver((mutations) => {
for (const mutation of mutations)
if (mutation.type === "childList")
for (const node of mutation.addedNodes) {
if (node.tagName === "TR") {
const children = [...node.children];
const record = children.find((child) => child.className === "record");
if (record) desktopHandleRecord(record);
}
}
});
// This is done to enchance support with Pagetual because it inserts elements from the next page and this update needs to be factored
const pElementObserver = new MutationObserver((mutations) => {
for (const mutation of mutations)
if (mutation.type === "childList")
for (const node of mutation.addedNodes)
if (node instanceof HTMLParagraphElement) mobileHandlePElement(node);
});
const affiliateRedirect = "https://www.etools.ch/redirect.do?a=";
if (config.displayFullLink || config.removeAffiliateLink) {
if (usingDesktop) {
const searchEntries = [...document.getElementsByClassName("record")];
for (const searchEntry of searchEntries) desktopHandleRecord(searchEntry);
recordObserver.observe(document.body, {
childList: true,
subtree: true,
});
}
if (usingMobile) {
const pElements = [...document.getElementsByTagName("p")].filter((p) =>
/^(\d\.)/.test(p.getAttribute("title"))
);
for (const pElement of pElements) mobileHandlePElement(pElement);
pElementObserver.observe(document.body, {
childList: true,
subtree: true,
});
}
}
if (config.autofill) {
const searchBar = document.getElementById("query");
if (searchBar) {
searchBar.autocomplete = "off";
const autofill = document.createElement("datalist");
autofill.id = "autofill";
searchBar.setAttribute("list", "autofill");
searchBar.insertAdjacentElement("afterend", autofill);
searchBar.addEventListener("input", (e) => {
if (config.autofill.service === "brave")
GM_fetch(
`https://search.brave.com/api/suggest?q=${e.target.value}&rich=false&source=web`,
{
method: "GET",
}
)
.then((resp) => resp.json())
.then((results) => {
const sgs = results[1];
// Clear previous suggestions
autofill.innerHTML = "";
// Add new suggestions
for (const sg of sgs) {
const option = document.createElement("option");
option.value = sg;
autofill.appendChild(option);
}
});
});
}
}