支持将 Pixiv “喜欢的标签”添加到搜索栏
// ==UserScript==
// @name Pixiv 喜欢标签快捷添加
// @namespace http://tampermonkey.net/
// @version 2025-9-28
// @description 支持将 Pixiv “喜欢的标签”添加到搜索栏
// @author ctrn43062
// @match https://www.pixiv.net/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=pixiv.net
// @grant none
// @license MIT
// ==/UserScript==
/**
* [终极方案] 通过直接调用组件的 onChange prop 来更新 React input 的值。
* 这种方法绕过了事件派发,直接与 React 的内部实例交互。
* @param {HTMLInputElement} element - 目标 input 元素。
* @param {string} value - 你想要设置的新值。
*/
function setReactValueByFiber(element, value) {
// 1. 查找附加在 DOM 元素上的 React props 对象
const reactPropsKey = Object.keys(element).find(key => key.startsWith('__reactProps$'));
if (!reactPropsKey) {
console.error("错误: 无法在目标元素上找到 React props。此网站可能使用了不同版本的 React 或有保护措施。");
return;
}
const props = element[reactPropsKey];
// 2. 检查 onChange 处理器是否存在
if (!props.onChange) {
console.error("错误: 目标元素的 props 中没有找到 'onChange' 函数。状态更新可能由父组件处理。");
return;
}
// 3. 更新 input 的 value 属性
// 这一步至关重要,因为 onChange 函数内部通常会通过 event.target.value 来读取新值
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
nativeInputValueSetter.call(element, value);
// 4. 创建一个模拟的事件对象
// 我们不需要派发它,只需要构建一个结构足够让 onChange 函数使用的对象
const simulatedEvent = {
target: element,
currentTarget: element,
};
// 5. 直接调用 onChange 函数!
try {
props.onChange(simulatedEvent);
console.log("成功通过直接调用 onChange 更新了 React 状态。");
} catch (error) {
console.error("调用 onChange 函数时出错:", error);
}
}
(function() {
'use strict';
const input = document.querySelector('.charcoal-text-field-input')
const waitForElement = (select, max_try=100, interval=20) => {
const delay = async () => {
return new Promise(resolve => {
setTimeout( () => resolve(), interval)
}
)
}
return new Promise(async (resolve, reject) => {
let el = null
do {
console.debug(`Searching ${select}, ${max_try} try remain`)
await delay(interval)
el = document.querySelectorAll(select)
} while ((!el || !el.length) && max_try--)
resolve(el)
}
)
}
input.addEventListener('keydown', (event) => {
// 检查按下的键是否是 'Enter'
// event.key 是现代浏览器推荐的用法
if (event.key === 'Enter') {
const newValue = input.value
setReactValueByFiber(input, newValue);
}
});
input.addEventListener('click', async () => {
const myTags = await waitForElement('.gtm-my-tag-link')
// assert myTags is not empty
console.log(myTags)
myTags.forEach(tag => {
const container = tag.parentElement
// 判断是否已有 append 按钮
if(container.getAttribute('data-append')) {
return
}
// 修改 parent 样式
Object.assign(container.style, {
'flex-direction': 'column',
'justify-content': 'center'
})
// 添加 append 按钮
const appendTagToSearchButton = document.createElement('span')
appendTagToSearchButton.textContent = '+ Append'
Object.assign(appendTagToSearchButton.style, {
'text-align': 'center',
'margin-top': '4px',
'cursor': 'pointer',
'white-space': 'nowrap'
})
appendTagToSearchButton.className = tag.className
appendTagToSearchButton.addEventListener('click', () => {
const search = document.querySelector('.charcoal-text-field-input')
const tagText = tag.innerText.split(/\s+/).pop().replace(/^#/, ' ')
const newValue = search.value + tagText
search.value = newValue
search.setAttribute('value', newValue)
})
container.setAttribute('data-append', true)
container.appendChild(appendTagToSearchButton)
})
})
})();