Greasy Fork is available in English.
Twitter (X) で片思いフォロー中のアカウントのみを表示できます。また、両思いフォロー中のアカウントは強調表示できます。
// ==UserScript==
// @name Twitter (X) 片思い表示
// @namespace https://kuds.win/
// @version 1.3
// @description Twitter (X) で片思いフォロー中のアカウントのみを表示できます。また、両思いフォロー中のアカウントは強調表示できます。
// @author KUDs
// @match https://twitter.com/*
// @match https://x.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
// @grant none
// @license GPL-3.0-or-later
// ==/UserScript==
(function () {
"use strict";
// デフォルト状態(非表示・ハイライト無し)
let isHiding = false;
let isHighlighting = false;
// 更新処理のスケジューリングフラグ
let updateScheduled = false;
// 共通スタイルのボタン作成
function createStyledButton(text, clickHandler) {
const button = document.createElement("button");
button.textContent = text;
Object.assign(button.style, {
border: "none",
borderRadius: "20px",
padding: "10px 20px",
marginTop: "10px",
cursor: "pointer",
transition: "filter 0.3s, transform 0.3s",
});
button.addEventListener("mouseover", () => {
button.style.filter = "brightness(1.25)";
button.style.transform = "scale(1.05)";
});
button.addEventListener("mouseout", () => {
button.style.filter = "brightness(1)";
button.style.transform = "scale(1)";
});
button.addEventListener("click", clickHandler);
return button;
}
// 片思い表示ボタンのクリックハンドラ
function handleHideButtonClick() {
isHiding = !isHiding;
hideButton.textContent = isHiding ? "片思い ONLY: ON" : "片思い ONLY: OFF";
hideButton.style.backgroundColor = isHiding ? "skyblue" : "gray";
scheduleDOMUpdate();
}
// 両思いハイライトボタンのクリックハンドラ
function handleHighlightButtonClick() {
isHighlighting = !isHighlighting;
highlightButton.textContent = isHighlighting
? "両思い HIGHLIGHT: ON"
: "両思い HIGHLIGHT: OFF";
highlightButton.style.backgroundColor = isHighlighting ? "pink" : "gray";
scheduleDOMUpdate();
}
const hideButton = createStyledButton(
"片思い ONLY: OFF",
handleHideButtonClick
);
hideButton.style.backgroundColor = "gray";
const highlightButton = createStyledButton(
"両思い HIGHLIGHT: OFF",
handleHighlightButtonClick
);
highlightButton.style.backgroundColor = "gray";
highlightButton.style.display = "none";
// ボタン配置の初期化
function initializeButtons() {
const targetNavElement = document.querySelector(
'[data-testid="SideNav_NewTweet_Button"]'
);
if (targetNavElement) {
const parentNavElement = targetNavElement.parentNode;
if (parentNavElement && !hideButton.parentNode) {
parentNavElement.appendChild(hideButton);
parentNavElement.appendChild(highlightButton);
}
}
}
// cellInnerDiv の表示更新
function updateCellInnerDivVisibility(node) {
if (
node.textContent.includes("フォローされています") ||
node.textContent.includes("両思いです♡")
) {
node.style.display = isHiding ? "none" : "";
}
}
// userFollowIndicator の更新
function updateFollowIndicator(node) {
if (
node.textContent.trim() === "フォローされています" ||
node.textContent.trim() === "両思いです♡"
) {
const spanElement = node.querySelector("span");
if (isHighlighting) {
node.style.backgroundColor = "pink";
if (spanElement) spanElement.textContent = "両思いです♡";
} else {
node.style.backgroundColor = "";
if (spanElement) spanElement.textContent = "フォローされています";
}
}
}
// 一括して DOM を更新する処理
function performDOMUpdate() {
// 各 cellInnerDiv の更新
document
.querySelectorAll('div[data-testid="cellInnerDiv"]')
.forEach(updateCellInnerDivVisibility);
// 各 userFollowIndicator の更新
document
.querySelectorAll('[data-testid="userFollowIndicator"]')
.forEach(updateFollowIndicator);
// ボタンの表示切替(片思い表示時はハイライトボタンを非表示)
highlightButton.style.display = isHiding ? "none" : "";
initializeButtons();
updateScheduled = false;
}
// 更新をスケジュールする(複数回のミューテーションがあっても1フレーム内にまとめて実行)
function scheduleDOMUpdate() {
if (!updateScheduled) {
updateScheduled = true;
requestAnimationFrame(performDOMUpdate);
}
}
// MutationObserver で DOM の変化を監視
const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
// 新規追加ノードがボタンの配置対象の場合
if (
node.matches &&
node.matches('[data-testid="SideNav_NewTweet_Button"]')
) {
initializeButtons();
}
// cellInnerDiv の処理
if (node.matches && node.matches('div[data-testid="cellInnerDiv"]')) {
updateCellInnerDivVisibility(node);
}
// userFollowIndicator の処理
if (
node.matches &&
node.matches('[data-testid="userFollowIndicator"]')
) {
updateFollowIndicator(node);
}
});
}
});
scheduleDOMUpdate();
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
// 初期化
initializeButtons();
})();