Mayday.Blue 页面查找+歌词字体修改
// ==UserScript==
// @name Mayday.Blue 页面查找+歌词字体修改
// @namespace https://github.com/freysu
// @version 0.1
// @description Mayday.Blue 页面查找功能+歌词字体修改
// @author FreySu
// @match https://mayday.blue/*
// @icon https://mayday.blue/favicon.ico
// @grant GM_addStyle
// @require https://cdn.jsdelivr.net/npm/mark.js/dist/mark.min.js
// @require https://cdn.jsdelivr.net/npm/mousetrap
// ==/UserScript==
(function() {
'use strict';
var parent = document
// 防抖函数
function debounce(func, delay) {
let timeoutId;
return function() {
clearTimeout(timeoutId);
timeoutId = setTimeout(func, delay);
};
}
const listingClassNameTarget = {
"list":"div[data-astro-cid-j7pv25f6]",
"lyric":"article.leading-snug"
}
// 放置所有相关变量
const state = {
markInstance: null,
currentIndex: -1,
allArr: [],
keywords: [],
matches: null,
};
// 放置DOM元素
const elements = {
searchInput: parent.createElement('input'),
prevButton: parent.createElement('button'),
nextButton: parent.createElement('button'),
};
// 定义选项常量
const MARKJS_OPTIONS = {
element: 'span',
exclude: ['script', 'select', 'textarea'],
separateWordSearch: false,
diacritics: true,
synonyms: {},
accuracy: {
value: 'partially',
limiters: [",", ".", "-", "_", "/", "+", "(", ")", " ", "<", ">", "「", "」"],
},
caseSensitive: false,
wholeWord: true,
wildcards: false,
acrossElements: false,
ignoreJoiners: true,
ignorePunctuation: [",", ".", "?", "!", ";", ":", "-", "–", "—", "(", ")"],
each: function(node) {
state.allArr.push({ text: node.innerText });
state.currentIndex += 1;
},
done: function(totalMatches) {
console.log('Marking done!');
console.log(state.allArr);
console.log(totalMatches);
state.matches = totalMatches;
const toLowerCase = (str) => str.toLowerCase();
let keywordCounts = {};
const lowercasedItems = state.allArr.map(({ text }) => toLowerCase(text));
for (const text of lowercasedItems) {
keywordCounts[text] = lowercasedItems.filter((t) => t === text).length;
}
console.log(keywordCounts);
},
debug: false,
log: window.console
};
// 初始化Mark.js实例
function initMarkInstance() {
const { list, lyric } = listingClassNameTarget;
const elm = parent.querySelectorAll([list, lyric]);
state.markInstance = new Mark(elm);
}
// 清除标记
function clearMark() {
state.markInstance.unmark();
state.allArr.length = 0;
}
// 搜索匹配项
function searchMatches() {
const keyword = state.keywords[0];
clearMark();
if (keyword) {
state.markInstance.mark(state.keywords, { className: 'frey_highlight_4', ...MARKJS_OPTIONS });
}
}
// 获取下一个匹配项
function getNext(flg) {
const count = parent.querySelectorAll(".frey_highlight_4").length;
if (count === 0) {
return;
}
if (flg !== 0) {
state.currentIndex += flg;
}
if (state.currentIndex >= count) {
state.currentIndex = 0;
}
if (state.currentIndex < 0) {
state.currentIndex = count - 1;
}
document.querySelectorAll(".frey_highlight_4")[state.currentIndex].scrollIntoView({ behavior: 'smooth' });
}
// 输入事件处理
function handleInput() {
const keyword = elements.searchInput.value.trim();
state.keywords.length = 0;
state.keywords.push(keyword);
searchMatches();
}
// 添加事件监听器
elements.searchInput.addEventListener('input', debounce(handleInput, 300));
elements.prevButton.addEventListener("click", function() { getNext(-1) });
elements.nextButton.addEventListener("click", function() { getNext(1) });
function render(){
// 初始化Mark.js实例
initMarkInstance();
Mousetrap.bind("/",function(){
elements.searchInput.focus()
return false;
})
// 创建DOM元素
const searchDiv = parent.createElement('div');
searchDiv.className = 'flex items-center gap-2';
const searchContainer = document.createElement('div');
searchContainer.className = 'searchContainer';
elements.searchInput.type = 'text';
elements.searchInput.placeholder = 'Type [/] to Search ...';
elements.searchInput.style.marginRight = '10px';
elements.prevButton.textContent = 'Prev';
elements.prevButton.style.marginRight = '10px';
elements.nextButton.textContent = 'Next';
// 获取页面底部的容器
const container = parent.querySelector('footer');
// 添加DOM元素到容器
searchContainer.appendChild(elements.searchInput);
searchContainer.appendChild(elements.prevButton);
searchContainer.appendChild(elements.nextButton);
searchDiv.appendChild(searchContainer);
container.appendChild(searchDiv);
// 搜索框样式
GM_addStyle(`.frey_highlight_4{color:black;padding:5px;background:pink;}.searchContainer{display:flex;align-items:center;margin:10px;}.searchContainer input[type="text"]{color:black;padding:6px;border:1px solid #ddd;border-radius:4px;font-size:12px;outline:none;transition:border-color 0.3s ease;}.searchContainer input[type="text"]:focus{border-color:#3498db;}.searchContainer button{padding:6px 12px;border:none;border-radius:4px;background-color:#3498db;color:white;font-size:12px;cursor:pointer;transition:background-color 0.3s ease;}.searchContainer button:hover{background-color:#2980b9;}`)
// 歌词字体修改
GM_addStyle(`.leading-snug{line-height:1.375;font-family:"华文行楷";}`)
}
main()
async function main(){
await getElement(parent,"footer")
render()
}
function getElement (parent, selector, timeout = 0) {
return new Promise((resolve) => {
let result = parent.querySelector(selector)
if (result) return resolve(result)
let timer
const mutationObserver =
window.MutationObserver ||
window.WebkitMutationObserver ||
window.MozMutationObserver
if (mutationObserver) {
const observer = new mutationObserver((mutations) => {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
if (addedNode instanceof Element) {
result = addedNode.matches(selector)
? addedNode
: addedNode.querySelector(selector)
if (result) {
observer.disconnect()
timer && clearTimeout(timer)
return resolve(result)
}
}
}
}
})
observer.observe(parent, {
childList: true,
subtree: true
})
if (timeout > 0) {
timer = setTimeout(() => {
observer.disconnect()
return resolve(null)
}, timeout)
}
} else {
const listener = (e) => {
if (e.target instanceof Element) {
result = e.target.matches(selector)
? e.target
: e.target.querySelector(selector)
if (result) {
parent.removeEventListener('DOMNodeInserted', listener, true)
timer && clearTimeout(timer)
return resolve(result)
}
}
}
parent.addEventListener('DOMNodeInserted', listener, true)
if (timeout > 0) {
timer = setTimeout(() => {
parent.removeEventListener('DOMNodeInserted', listener, true)
return resolve(null)
}, timeout)
}
}
})
}
})()