Hide posts by keywords with the dashboard and posts from verified accounts
// ==UserScript==
// @name Twitter XFilter panel 2.4.67 (c) tapeavion
// @version 2.4.67
// @description Hide posts by keywords with the dashboard and posts from verified accounts
// @author gullampis810
// @match https://x.com/*
// @match https://x.com/i/grok*
// @match https://blank.org/*
// @grant GM_addStyle
// @run-at document-end
// @license MIT
// @icon https://www.pinclipart.com/picdir/big/450-4507608_twitter-circle-clipart.png
// @namespace http://tampermonkey.net/
// ==/UserScript==
(function () {
"use strict";
// ========== Настройки и инициализация ========== //
const STORAGE_KEY = "hiddenKeywords";
const FAVORITE_USERS_KEY = "favoriteUsers";
const BLOCKED_USERS_KEY = "blockedUsers";
// NEW: Новый ключ для черного списка GIF
const GIF_BLACKLIST_KEY = "gifBlacklist";
let hiddenKeywords = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
let favoriteUsers = JSON.parse(localStorage.getItem(FAVORITE_USERS_KEY)) || [];
let blockedUsers = JSON.parse(localStorage.getItem(BLOCKED_USERS_KEY)) || [];
let unblockedKeywords = JSON.parse(localStorage.getItem("unblockedKeywords")) || [];
// NEW: Инициализация черного списка для GIF
// Глобальные переменные (добавьте в начало скрипта, если их нет)
// Глобальные переменные (добавьте это вне функции, в начале скрипта)
let filteredData = []; // Полный отфильтрованный массив данных
let itemHeight = 40; // Высота одного элемента (в пикселях, соответствует CSS)
let viewportHeight = 400; // Высота viewport (#keywordList)
let maxItems = 10; // Максимальное количество элементов для рендера
let gifBlacklist = JSON.parse(localStorage.getItem(GIF_BLACKLIST_KEY)) || [];
let hideVerifiedAccounts = false;
let hideNonVerifiedAccounts = JSON.parse(localStorage.getItem("hideNonVerifiedAccounts")) || false;
let displayMode = "keywords"; // keywords, favorites, blocked
const languageFilters = {
english: /[a-zA-Z]/,
russian: /[А-Яа-яЁё]/,
japanese: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u,
ukrainian: /[А-Яа-яІіЄєЇїҐґ]/,
belarusian: /[А-Яа-яЎўЁёІі]/,
tatar: /[А-Яа-яӘәӨөҮүҖҗ]/,
mongolian: /[\p{Script=Mongolian}]/u,
chinese: /[\p{Script=Han}]/u,
german: /[a-zA-ZßÄäÖöÜü]/,
polish: /[a-zA-ZąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/,
french: /[a-zA-Zàâçéèêëîïôûùüÿ]/,
swedish: /[a-zA-ZåäöÅÄÖ]/,
estonian: /[a-zA-ZäõöüÄÕÖÜ]/,
danish: /[a-zA-Z帿ŨÆ]/,
turkish: /[a-zA-ZıİçÇğĞöÖşŞüÜ]/,
portuguese: /[a-zA-Zàáâãçéêíóôõúü]/,
};
let activeLanguageFilters = {};
// =============== showNotification ================ //
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.style.position = 'fixed';
notification.style.top = '20px';
notification.style.right = '20px';
notification.style.padding = '10px 20px';
notification.style.borderRadius = '8px';
notification.style.zIndex = '10001';
notification.style.color = 'white';
notification.style.fontFamily = 'Arial, sans-serif';
switch(type) {
case 'success':
notification.style.background = 'linear-gradient(90deg,rgb(31, 90, 33), #81C784)';
break;
case 'error':
notification.style.background = 'linear-gradient(90deg,rgb(94, 41, 38),rgb(212, 134, 121))';
break;
default:
notification.style.background = 'linear-gradient(90deg,rgb(33, 70, 100), #64B5F6)';
}
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.transition = 'opacity 0.5s';
notification.style.opacity = '0';
setTimeout(() => {
notification.remove();
}, 500);
}, 2000);
}
// Получение имени пользователя из профиля
function getUsernameFromProfile() {
const profileUrl = window.location.href;
const match = profileUrl.match(/x\.com\/([^\?\/]+)/);
if (match && match[1]) {
return match[1].toLowerCase();
}
const usernameElement = document.querySelector('a[href*="/"][role="link"] div[dir="ltr"] span');
return usernameElement ? usernameElement.textContent.replace('@', '').toLowerCase() : '';
}
// ========== Сохранение в localStorage ========== //
function saveKeywords() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(hiddenKeywords));
}
function saveFavoriteUsers() {
localStorage.setItem(FAVORITE_USERS_KEY, JSON.stringify(favoriteUsers));
}
function saveBlockedUsers() {
localStorage.setItem(BLOCKED_USERS_KEY, JSON.stringify(blockedUsers));
}
function saveUnblockedKeywords() {
localStorage.setItem("unblockedKeywords", JSON.stringify(unblockedKeywords));
}
// NEW: Сохранение черного списка GIF
function saveGifBlacklist() {
localStorage.setItem(GIF_BLACKLIST_KEY, JSON.stringify(gifBlacklist));
}
// ========== Функция для обновления фильтров по языкам ========== //
function updateLanguageFilter(language) {
if (activeLanguageFilters[language]) {
delete activeLanguageFilters[language];
} else {
activeLanguageFilters[language] = languageFilters[language];
}
hidePosts();
}
// ========== Проверка языка текста ========== //
function isTextInLanguage(text) {
for (const [language, regex] of Object.entries(activeLanguageFilters)) {
if (regex.test(text)) {
return true;
}
}
return false;
}
// ========== Функция скрытия постов ========== //
function hidePosts() {
document.querySelectorAll("article").forEach((article) => {
const textContent = article.innerText.toLowerCase();
const isVerifiedAccount = article.querySelector('[data-testid="icon-verified"]');
const usernameElement = article.querySelector('a[href*="/"]');
const usernameFromSpan = article.querySelector('a[href*="/"] div[dir="ltr"] span')?.textContent.replace('@', '').toLowerCase() || '';
const username = usernameElement ? usernameElement.getAttribute("href").slice(1).toLowerCase() : usernameFromSpan;
// Проверяем, является ли пользователь избранным
const isFavoriteUser = favoriteUsers.includes(username);
// Проверяем, является ли пользователь заблокированным
const isBlockedUser = blockedUsers.includes(username);
// Если пользователь в избранных, показываем пост
if (isFavoriteUser) {
article.style.display = "";
// NEW: Добавляем элементы управления для GIF
addGifControls(article);
return;
}
// Если пользователь заблокирован, скрываем пост
if (isBlockedUser) {
article.style.display = "none";
return;
}
// Проверка на ключевые слова
const matchesKeyword = hiddenKeywords.some((keyword) => {
try {
return new RegExp(keyword, "i").test(textContent);
} catch (e) {
return textContent.includes(keyword.toLowerCase());
}
});
// Проверка на запрещенные GIF
let matchesGif = false;
const gifElements = article.querySelectorAll('video[aria-label], img[alt="GIF"]');
gifElements.forEach((gif) => {
const gifLabel = gif.getAttribute('aria-label')?.toLowerCase() || '';
const gifSrc = gif.getAttribute('src')?.toLowerCase() || '';
const gifPoster = gif.getAttribute('poster')?.toLowerCase() || '';
const gifContent = gifLabel + gifSrc + gifPoster;
matchesGif = gifBlacklist.some((gifKeyword) => gifContent.includes(gifKeyword.toLowerCase()));
if (matchesGif) return;
});
// Проверка на посты без текста (только GIF)
const hasOnlyGif = gifElements.length > 0 &&
!article.querySelector('[data-testid="tweetText"]') &&
textContent.trim().replace(/[@\w]+|\d+\s?[чмдн]/gi, '').trim().length < 10;
// Логика скрытия постов
const shouldHideVerified = hideVerifiedAccounts && isVerifiedAccount;
const shouldHideNonVerified = hideNonVerifiedAccounts && !isVerifiedAccount;
const shouldHideByLanguage = isTextInLanguage(textContent);
const shouldHideByKeyword = matchesKeyword;
const shouldHideByGif = matchesGif || hasOnlyGif;
if (shouldHideVerified || shouldHideNonVerified || shouldHideByLanguage || shouldHideByKeyword || shouldHideByGif) {
article.style.display = "none";
} else {
article.style.display = "";
// NEW: Добавляем элементы управления для GIF, если пост не скрыт
addGifControls(article);
}
});
}
// NEW: Функция для добавления элементов управления GIF
function addGifControls(article) {
const gifElements = article.querySelectorAll('video[aria-label]');
gifElements.forEach((gif) => {
// Проверяем, существует ли уже контейнер управления
if (gif.parentElement.querySelector('.gif-control-container')) {
return; // Пропускаем, если контейнер уже есть
}
const gifLabel = gif.getAttribute('aria-label') || 'GIF';
if (!gifLabel) return; // Пропускаем, если нет aria-label
// Создаем контейнер для элементов управления
const container = document.createElement('div');
container.className = 'gif-control-container';
// Добавляем текст с названием GIF
const label = document.createElement('span');
label.className = 'gif-label';
label.textContent = gifLabel;
container.appendChild(label);
// Добавляем кнопку "Копировать"
const copyButton = document.createElement('button');
copyButton.textContent = 'copy gif name'; // копировать
copyButton.addEventListener('click', () => {
navigator.clipboard.writeText(gifLabel).then(() => {
showNotification(`Скопировано: ${gifLabel}`, 'success');
}).catch(() => {
showNotification('Ошибка копирования', 'error');
});
});
container.appendChild(copyButton);
// Вставляем контейнер в родительский элемент видео
gif.parentElement.style.position = 'relative';
gif.parentElement.appendChild(container);
});
}
// ========== Debounce для оптимизации ========== //
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
const debouncedHidePosts = debounce(hidePosts, 200);
// ========== Создание панели управления ========== //
const panel = document.createElement("div");
panel.id = "xpanelxfilter-xai-he5h5eh-controlPanel"; // Здесь задаем ID для панели (можно использовать для CSS или querySelector)
panel.style.position = "fixed";
panel.style.left = "1015px"; // Заменяем right на left
panel.style.top = "89px"; // Заменяем bottom на top
panel.style.width = "335px";
panel.style.height = "710px";
panel.style.padding = "8px";
panel.style.fontFamily = "Arial, sans-serif";
panel.style.backgroundColor = "#34506c";
panel.style.color = "#fff";
panel.style.borderRadius = "8px";
panel.style.boxShadow = "0 0 10px rgba(0,0,0,0.5)";
panel.style.zIndex = "9999";
panel.style.overflow = "auto";
panel.style.transition = "height 0.3s ease";
// Принудительное применение стилей скроллбара для панели
panel.style.scrollbarWidth = 'auto';
panel.style.scrollbarColor = '#c1a5ef #205151';
// NEW: Добавляем счетчик для GIF в панель
panel.innerHTML = `
<h3 style="
margin: 0;
font-size: 16px;
" id="panelHeader"> XFilter </h3>
<div style="
display: flex;
align-items: center;
gap: 20px;
top: 20px;
position: relative;
width: 250px;
left: 10px;
">
<span id="counter-keywords" class="counter-span">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16">
<title>File-earmark-text-fill SVG Icon</title>
<path fill="currentColor" d="M9.293 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0M9.5 3.5v-2l3 3h-2a1 1 0 0 1-1-1M4.5 9a.5.5 0 0 1 0-1h7a.5.5 0 0 1 0 1zM4 10.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m.5 2.5a.5.5 0 0 1 0-1h4a.5.5 0 0 1 0 1z"/>
</svg>
<span class="counter-number">0</span>
<span class="counter-tooltip">Keywords</span>
</span>
<span id="counter-favorites" class="counter-span">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 32 32">
<title>User-favorite SVG Icon</title>
<path fill="currentColor" d="M27.303 12a2.662 2.662 0 0 0-1.908.806l-.393.405l-.397-.405a2.662 2.662 0 0 0-3.816 0a2.8 2.8 0 0 0 0 3.896L25.002 21l4.209-4.298a2.8 2.8 0 0 0 0-3.896A2.662 2.662 0 0 0 27.303 12M2 30h2v-5a5.006 5.006 0 0 1 5-5h6a5.006 5.006 0 0 1 5 5v5h2v-5a7.008 7.008 0 0 0-7-7H9a7.008 7.008 0 0 0-7 7zM12 4a5 5 0 1 1-5 5a5 5 0 0 1 5-5m0-2a7 7 0 1 0 7 7a7 7 0 0 0-7-7"/>
</svg>
<span class="counter-number">0</span>
<span class="counter-tooltip">Favorite Users</span>
</span>
<span id="counter-blocked" class="counter-span">
<svg class="counter-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="8.5" cy="7" r="4"/>
<line x1="18" y1="8" x2="22" y2="12"/>
<line x1="22" y1="8" x2="18" y2="12"/>
</svg>
<span class="counter-number">0</span>
<span class="counter-tooltip">Blocked Users</span>
</span>
<span id="counter-gif" class="counter-span">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16">
<title>Filetype-gif SVG Icon</title>
<path fill="currentColor" fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2H9v-1h3a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM3.278 13.124a1.4 1.4 0 0 0-.14-.492a1.3 1.3 0 0 0-.314-.407a1.5 1.5 0 0 0-.48-.275a1.9 1.9 0 0 0-.636-.1q-.542 0-.926.229a1.5 1.5 0 0 0-.583.632a2.1 2.1 0 0 0-.199.95v.506q0 .408.105.745q.105.336.32.58q.213.243.533.377q.323.132.753.132q.402 0 .697-.111a1.29 1.29 0 0 0 .788-.77q.097-.261.097-.551v-.797H1.717v.589h.823v.255q0 .199-.09.363a.67.67 0 0 1-.273.264a1 1 0 0 1-.457.096a.87.87 0 0 1-.519-.146a.9.9 0 0 1-.305-.413a1.8 1.8 0 0 1-.096-.615v-.499q0-.547.234-.85q.237-.3.665-.301a1 1 0 0 1 .3.044q.136.044.236.126a.7.7 0 0 1 .17.19a.8.8 0 0 1 .097.25zm1.353 2.801v-3.999H3.84v4h.79Zm1.493-1.59v1.59h-.791v-3.999H7.88v.653H6.124v1.117h1.605v.638z"/>
</svg>
<span class="counter-number">0</span>
<span class="counter-tooltip">GIF Blacklist</span>
</span>
</div>
<h4 class="version-text">v2.4.67</h4>
<div style="display: flex;
align-items: center;
gap: 5px;
margin: 10px 0;
">
<input id="keywordInput" type="text" placeholder="Enter the word, @username or gif:keyword" style="
width: calc(100% - 95px);
height: 35px;
padding: 0px;
border-radius: 5px;
border: none;
background: #15202b;
color: #fff;
">
<button id="addKeyword" style="
min-width: 75px;
max-width: 80px;
background: #203142;
color: #a9b6c4;
border: none;
border-radius: 5px;
height: 35px;
font-size: 20px;
">Add it</button>
</div>
<div style="display: flex;
flex-wrap: wrap;
gap: 5px;
position: relative;
">
<div style="display: flex;
align-items: center;
gap: 5px;
">
<div id="searchWrapper" style="position: relative;
">
<input id="searchInput" type="text" placeholder="Search keywords or users" style="width: 240px;
width: 235px;
height: 35px;
border-radius: 5px;
border: none;
background-color: #15202b;
color: #fff;
">
<span id="clearSearch" style="display: none;
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #fff;
cursor: pointer;
"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></span>
</div>
<button id="openLanguagePopup" style="
width: 75px;
background: #203142;
color: #a9b6c4;
border: none;
border-radius: 5px;
height: 35px;
font-size: 14px;
font-weight: bold;
">Language Filtering</button>
</div>
<button id="exportKeywords" style="flex: 1;
min-width: 75px;
max-width: 70px;
background: #203142;
color: #a9b6c4;
border: none;
border-radius: 5px;
height: 40px;
">Export</button>
<button id="importKeywords" style="flex: 1;
min-width: 75px;
max-width: 70px;
background: #203142;
color: #a9b6c4;
border: none;
border-radius: 5px;
height: 40px;
">Import</button>
<button id="toggleBlockKeywords" style="flex: 1;
min-width: 80px;
max-width: 90px;
background: #203142;
color: #a9b6c4;
border: none;
border-radius: 5px;
height: 40px;
font-size: 13px;
font-weight: bold;
">Unblock All</button>
<button id="clearKeywords" style="flex: 1;
min-width: 75px;
max-width: 80px;
background: #203142;
color: #a9b6c4;
border: none;
border-radius: 5px;
height: 40px;
">Clear all</button>
<button id="toggleFavoriteUsers" style="width: 80px;
background: #203142;
color: #a9b6c4;
border: none;
border-radius: 5px;
height: 40px;
font-size: 13px;
font-weight: bold;
">Favorite Users</button>
<button id="toggleBlockedUsers" style="width: 80px;
background: #203142;
color: #a9b6c4;
border: none;
border-radius: 5px;
height: 40px;
font-size: 13px;
font-weight: bold;
">Blocked Users</button>
<button id="toggleVerifiedPosts" style="width: 242px;
background: #203142;
color: #a9b6c4;
border: none;
border-radius: 5px;
height: 40px;
font-size: 13px;
font-weight: bold;
">Hide verified accounts: Click to Enable</button>
<button id="toggleNonVerifiedPosts" style="width: 242px;
background: #203142;
color: #a9b6c4;
border: none;
border-radius: 5px;
height: 40px;
font-size: 13px;
font-weight: bold;
">Hide non-verified accounts: Turn ON</button>
</div>
<div id="listLabel" style="
width: 222px;
color: #15202b !important;
font-size: 18px !important;
top: 30px;
position: inherit;
font-weight: 700;
">List of Keywords</div>
<ul id="keywordList" style="
list-style: none;
padding: 0px;
font-size: 14px;
color: rgb(255, 255, 255);
max-height: 275px;
overflow-y: auto;
background-color: rgb(21, 32, 43);
box-shadow: inset rgb(0 0 0 / 31%) 0px 11px 24px 0px;
user-select: text;
top: 19px;
"></ul>
`;
document.body.appendChild(panel);
// ========== Функция обновления счетчиков ========== //
function updateCounters() {
document.querySelector("#counter-keywords .counter-number").textContent = hiddenKeywords.length;
document.querySelector("#counter-favorites .counter-number").textContent = favoriteUsers.length;
document.querySelector("#counter-blocked .counter-number").textContent = blockedUsers.length;
document.querySelector("#counter-gif .counter-number").textContent = gifBlacklist.length;
}
const searchInput = document.getElementById("searchInput");
const clearSearch = document.getElementById("clearSearch");
searchInput.addEventListener("input", () => {
clearSearch.style.display = searchInput.value.trim() ? "block" : "none";
updateKeywordList();
updateCounters();
});
clearSearch.addEventListener("click", () => {
searchInput.value = "";
clearSearch.style.display = "none";
updateKeywordList();
updateCounters();
});
const lengthFilterInput = document.createElement("input");
lengthFilterInput.type = "number";
lengthFilterInput.placeholder = "Min length";
lengthFilterInput.style.width = "80px";
lengthFilterInput.style.marginTop = "10px";
panel.appendChild(lengthFilterInput);
lengthFilterInput.addEventListener("change", () => {
debouncedHidePosts();
});
// ========== Перетаскивание панели ========== //
let isDragging = false;
let offsetX = 0;
let offsetY = 0;
panel.addEventListener("mousedown", (event) => {
// Проверяем, выделен ли текст
if (window.getSelection().toString()) {
return; // Если текст выделен, не начинаем перетаскивание
}
// Проверяем, что клик не по элементу управления
const target = event.target;
if (
target.tagName === "INPUT" ||
target.tagName === "BUTTON" ||
target.tagName === "UL" ||
target.closest("#keywordList") ||
target.closest("#clearSearch")
) {
return; // Не начинаем перетаскивание
}
isDragging = true;
offsetX = event.clientX - panel.offsetLeft;
offsetY = event.clientY - panel.offsetTop;
panel.style.cursor = "grabbing";
});
document.addEventListener("mousemove", (event) => {
if (isDragging) {
panel.style.left = event.clientX - offsetX + "px";
panel.style.top = event.clientY - offsetY + "px";
}
});
document.addEventListener("mouseup", () => {
isDragging = false;
panel.style.cursor = "move";
});
// ========== Создание попапа для языков ========== //
const languagePopup = document.createElement("div");
languagePopup.style.display = "none";
languagePopup.style.position = "fixed";
languagePopup.style.top = "100px";
languagePopup.style.right = "65px";
languagePopup.style.transform = "translate(-145%, 45%)";
languagePopup.style.backgroundColor = " #34506c";
languagePopup.style.padding = "20px";
languagePopup.style.borderRadius = "8px";
languagePopup.style.zIndex = "10000";
languagePopup.style.width = "288px";
languagePopup.style.boxShadow = "0 0 10px rgba(0,0,0,0.5)";
languagePopup.style.fontFamily = "Arial, sans-serif";
for (const language in languageFilters) {
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = `lang-${language}`;
checkbox.name = language;
const label = document.createElement("label");
label.htmlFor = `lang-${language}`;
label.textContent = language.charAt(0).toUpperCase() + language.slice(1);
const wrapper = document.createElement("div");
wrapper.appendChild(checkbox);
wrapper.appendChild(label);
languagePopup.appendChild(wrapper);
}
const closeButton = document.createElement("button");
closeButton.textContent = "X";
closeButton.style.fontSize = "22px";
closeButton.style.position = "relative";
closeButton.style.width = "40px";
closeButton.style.height = "40px";
closeButton.style.borderRadius = "50%";
closeButton.style.backgroundColor = "#203142";
closeButton.style.color = " rgb(237 131 123) ";
closeButton.style.border = "none";
closeButton.style.display = "flex";
closeButton.style.alignItems = "center";
closeButton.style.justifyContent = "center";
closeButton.style.cursor = "pointer";
closeButton.style.left = "1%";
closeButton.style.top = "20px";
closeButton.addEventListener("click", () => {
languagePopup.style.display = "none";
});
languagePopup.appendChild(closeButton);
document.body.appendChild(languagePopup);
document.getElementById("openLanguagePopup").addEventListener("click", () => {
languagePopup.style.display = "block";
});
const warningText = document.createElement("div");
warningText.textContent = "⚠️ Careful, it might stop working if it lags";
warningText.style.color = " #ffcc00";
warningText.style.fontSize = "14px";
warningText.style.marginBottom = "10px";
warningText.style.textAlign = "end";
warningText.style.right = "0px";
warningText.style.position = "relative";
warningText.style.top = "25px";
warningText.style.width = "260px";
languagePopup.appendChild(warningText);
for (const language in languageFilters) {
document.getElementById(`lang-${language}`).addEventListener("change", () => {
updateLanguageFilter(language);
});
}
// ========== Стили для подсветки, скроллбара и выравнивания кнопок ========== //
const style = document.createElement("style");
style.textContent = `
button {
transition: box-shadow 0.3s, transform 0.3s, background-color 0.3s;
}
button:hover {
box-shadow: 0 0 10px rgba(255, 255, 255, 0.7);
}
button:active {
transform: scale(0.95);
box-shadow: 0 0 5px rgba(255, 255, 255, 0.7);
}
button.active,
#toggleVerifiedPosts.active,
#toggleNonVerifiedPosts.active,
#toggleFavoriteUsers.active,
#toggleBlockedUsers.active {
background: linear-gradient(5deg, rgb(74 169 117), rgb(7 100 54)) !important;
box-shadow: 0 0 8px rgb(216 240 253 / 51%) !important;
color: rgb(21 32 43) !important;
}
.version-text {
left: 275px;
position: relative;
bottom: 18px;
color: #15202b;
margin: 0;
font-size: 14px;
width: 47px;
}
#keywordInput {
cursor: pointer;
background: #15202b;
}
#favoriteUserButton:hover img {
filter: brightness(1.2);
}
#favoriteUserButton:active img {
transform: scale(0.9);
}
#keywordList button {
background-color: transparent; /* Убираем красный фон */
color: #f44336; /* Цвет иконки (красный, как раньше) */
border: none;
border-radius: 3px;
cursor: pointer;
width: 24px; /* Фиксированная ширина для SVG */
height: 24px; /* Фиксированная высота для SVG */
display: flex;
align-items: center;
justify-content: center;
padding: 0; /* Убираем отступы для центрирования */
}
ul#keywordList {
width: 325px !important;
}
#keywordList li {
width: 290px !important;
}
#keywordList li {
border-top: 2px solid #1c6596;
color: rgb(159 173 182);
}
#keywordList {
border: 1px solid #1c6596;
border-radius: 10px;
background-color: rgb(21, 32, 43);
}
h3#panelHeader {
width: 140px;
color: #15202b;
}
#keywordList {
width: 300px !important !important;
overflow-x: hidden !important;
}
#keywordList li {
border-top: 2px solid #1c6596;
color: rgb(136 151 161);
}
#keywordList li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
}
#keywordList li button {
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
}
#keywordList {
height: 400px; /* Фиксированная высота viewport, подберите под дизайн */
overflow-y: auto; /* Скроллбар только по вертикали */
position: relative; /* Для абсолютного позиционирования элементов */
}
#keywordList li {
height: 40px; /* Фиксированная высота каждого элемента, для точного расчёта */
line-height: 40px; /* Выравнивание текста */
display: flex;
justify-content: space-between;
align-items: center;
}
/*------ NEW: Стили для контейнера GIF и кнопок---------- */
.gif-control-container {
position: absolute;
bottom: 65px;
left: 35px;
display: flex;
align-items: center;
gap: 5px;
background: rgb(0 0 0 / 49%);
padding: 0px 4px;
border-radius: 8px;
z-index: 1000;
font-family: Arial, sans-serif;
color: #87fff5;
font-size: 11px;
opacity: 0; /* Скрыт по умолчанию */
visibility: hidden; /* Скрыт по умолчанию */
transition: opacity 0.3s ease, visibility 0.3s ease; /* Плавное появление */
}
/* Показываем контейнер при наведении на элемент с data-testid="tweetPhoto" */
div[data-testid="tweetPhoto"]:hover .gif-control-container {
opacity: 1;
visibility: visible;
}
.gif-control-container .gif-label {
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gif-control-container button {
padding: 3px 5px;
background: #34506c;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 11px;
}
.gif-control-container button:hover {
background: #3b5a7b;
}
.gif-control-container button:active {
transform: scale(0.95);
}
/* Стили для кастомных тултипов */
.counter-span {
position: relative;
cursor: help; /* Курсор-подсказка при наведении */
}
#counter-keywords:hover,
#counter-gif:hover,
#counter-blocked:hover,
#counter-favorites:hover {
color: #00ffad !important;
scale: 1.5 !important;
}
#counter-favorites {
color: #ffc400;
}
#counter-blocked {
color: #ea7878;
}
#counter-gif {
color: rgb(45 154 118);
}
#counter-keywords {
color: #00a5ff;
}
.counter-tooltip {
visibility: hidden;
opacity: 0;
position: absolute;
bottom: 93%; /* Позиция над элементом */
left: 50%;
transform: translateX(-50%);
background-color: #15202b; /* Цвет фона */
color: #fff;
text-align: center;
border-radius: 4px;
padding: 5px 8px;
font-size: 12px;
white-space: nowrap;
z-index: 100000;
transition: opacity 0.3s, visibility 0.3s;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
border: 1px solid #203142;
}
.counter-span:hover .counter-tooltip {
visibility: visible;
opacity: 1;
}
.counter-tooltip::after {
content: "";
position: absolute;
top: 100%; /* Стрелка вниз */
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #15202b transparent transparent transparent;
}
.counter-icon {
width: 16px;
height: 16px;
vertical-align: middle;
margin-right: 2px;
}
#clearSearch svg {
width: 18px;
height: 18px;
}
input[id^="lang-"] {
appearance: none !important;
background: #2c4561 !important;
border: 2px solid rgb(21, 32, 43);
border-radius: 5px !important;
width: 20px;
height: 20px;
}
input[id^="lang-"]:checked {
background: #243a51 !important; /* Цвет при выборе */
border-color: #d7b415 !important; /* Зелёная рамка при выборе */
}
input[id^="lang-"]:checked::after {
content: "✓"; /* Галочка при выборе */
position: absolute;
left: 9%;
color: rgb(215 180 21);
font-size: 14px;
font-weight: bold;
}
label[for^="lang-"] {
cursor: pointer;
font-size: 14px;
margin-left: 5px;
}
div:has(input[id^="lang-"]) {
display: flex;
align-items: center;
margin-bottom: 10px;
}
/* Стили для скроллбара (замена GM_addStyle) */
#keywordList:hover::-webkit-scrollbar {
width: 25px !important;
}
#keywordList::-webkit-scrollbar {
width: 18px !important;
}
#keywordList {
scrollbar-width: auto;
scrollbar-color: auto ;
}
`;
document.head.appendChild(style);
// Принудительное применение стилей скроллбара (опционально, для немедленного эффекта)
const keywordList = document.getElementById("keywordList");
if (keywordList) {
keywordList.style.scrollbarColor = 'auto';
keywordList.style.scrollbarWidth = 'auto';
}
// ========== Кнопка переключения панели FilterX ========== //
let isSwitchOn = localStorage.getItem("isSwitchOn") === "true";
const toggleButton = document.createElement("div");
toggleButton.style.display = "flex";
toggleButton.style.alignItems = "center";
toggleButton.style.gap = "10px";
toggleButton.style.background = " #15202b";
toggleButton.style.border = "4px solid #6c7e8e";
toggleButton.style.borderRadius = "45px";
toggleButton.style.padding = "8px 12px";
toggleButton.style.marginTop = "10px";
toggleButton.style.cursor = "pointer";
toggleButton.style.width = "auto";
toggleButton.className = "css-175oi2r r-16y2uox";
function isGrokPage() {
return window.location.href.startsWith("https://x.com/i/grok");
}
const toggleLabel = document.createElement("label");
toggleLabel.style.display = "inline-block";
toggleLabel.style.width = "50px";
toggleLabel.style.height = "25px";
toggleLabel.style.borderRadius = "25px";
toggleLabel.style.backgroundColor = isSwitchOn ? " #425364" : " #0d1319";
toggleLabel.style.position = "relative";
toggleLabel.style.cursor = "pointer";
toggleLabel.style.transition = "background-color 0.3s";
toggleLabel.style.top = "0px";
toggleLabel.style.left = "75px";
const toggleSwitch = document.createElement("div");
toggleSwitch.style.position = "absolute";
toggleSwitch.style.width = "21px";
toggleSwitch.style.height = "21px";
toggleSwitch.style.borderRadius = "50%";
toggleSwitch.style.backgroundColor = " #6c7e8e";
toggleSwitch.style.top = "2px";
toggleSwitch.style.left = isSwitchOn ? "calc(100% - 23px)" : "2px";
toggleSwitch.style.transition = "left 0.3s ease";
toggleSwitch.style.boxShadow = "rgb(21, 32, 43) -1px 1px 4px 1px";
function toggleSwitchState(event) {
event.stopPropagation();
isSwitchOn = !isSwitchOn;
localStorage.setItem("isSwitchOn", isSwitchOn.toString());
toggleSwitch.style.left = isSwitchOn ? "calc(100% - 23px)" : "2px";
toggleLabel.style.backgroundColor = isSwitchOn ? " #425364" : " #0d1319";
if (isSwitchOn) {
panel.style.display = "block";
setTimeout(() => {
panel.style.height = "710px";
}, 10);
} else {
panel.style.height = "0px";
setTimeout(() => {
panel.style.display = "none";
}, 300);
}
isPanelVisible = isSwitchOn;
localStorage.setItem("panelVisible", isPanelVisible.toString());
}
const debouncedToggleSwitchState = debounce(toggleSwitchState, 300);
toggleButton.addEventListener("click", debouncedToggleSwitchState);
toggleLabel.appendChild(toggleSwitch);
toggleButton.appendChild(toggleLabel);
const toggleText = document.createElement("span");
toggleText.textContent = "FilterX";
toggleText.style.color = " #6c7e8e";
toggleText.style.fontFamily = "Arial, sans-serif";
toggleText.style.fontSize = "16px";
toggleText.style.fontWeight = "bold";
toggleText.style.position = "absolute";
toggleText.style.top = "12px";
toggleText.style.left = "25px";
toggleButton.appendChild(toggleText);
function waitForPostButton(callback) {
const interval = setInterval(() => {
const postButton = document.querySelector("[data-testid='SideNav_NewTweet_Button']");
const postButtonContainer = postButton?.parentElement;
if (postButtonContainer) {
clearInterval(interval);
callback(postButtonContainer);
} else {
console.warn("Контейнер кнопки 'Опубликовать пост' не найден");
}
}, 500);
}
waitForPostButton((postButtonContainer) => {
toggleButton.style.display = isGrokPage() ? "none" : "flex";
postButtonContainer.appendChild(toggleButton);
});
// ========== Управление высотой панели ========== //
let isPanelVisible = localStorage.getItem("panelVisible") === "true";
function togglePanel() {
if (isPanelVisible) {
panel.style.height = "0px";
setTimeout(() => {
panel.style.display = "none";
}, 300);
} else {
panel.style.display = "block";
setTimeout(() => {
panel.style.height = "710px";
}, 10);
}
isPanelVisible = !isPanelVisible;
localStorage.setItem("panelVisible", isPanelVisible.toString());
}
toggleButton.addEventListener("click", togglePanel);
if (isPanelVisible) {
panel.style.height = "710px";
panel.style.display = "block";
} else {
panel.style.height = "0px";
panel.style.display = "none";
}
// ========== Элементы управления ========== //
const addKeywordBtn = document.getElementById("addKeyword");
const clearKeywordsBtn = document.getElementById("clearKeywords");
const exportKeywordsBtn = document.getElementById("exportKeywords");
const importKeywordsBtn = document.getElementById("importKeywords");
const toggleVerifiedBtn = document.getElementById("toggleVerifiedPosts");
const toggleNonVerifiedBtn = document.getElementById("toggleNonVerifiedPosts");
const toggleBlockBtn = document.getElementById("toggleBlockKeywords");
const openLanguagePopupBtn = document.getElementById("openLanguagePopup");
const toggleFavoriteUsersBtn = document.getElementById("toggleFavoriteUsers");
const toggleBlockedUsersBtn = document.getElementById("toggleBlockedUsers");
// ========== Обработчики событий ========== //
// NEW: Модифицированный обработчик добавления с поддержкой gif:
addKeywordBtn.addEventListener("click", () => {
const inputValue = keywordInput.value.trim().toLowerCase();
if (inputValue) {
if (inputValue.startsWith("gif:")) {
const gifKeyword = inputValue.slice(4).trim();
if (gifKeyword && !gifBlacklist.includes(gifKeyword)) {
gifBlacklist.push(gifKeyword);
saveGifBlacklist();
showNotification(`Добавлено в черный список GIF: ${gifKeyword}`, 'success');
} else {
showNotification(`Ключевое слово GIF уже существует: ${gifKeyword}`, 'error');
}
} else if (displayMode === "favorites" && inputValue.startsWith("@")) {
const username = inputValue.slice(1);
if (!favoriteUsers.includes(username)) {
favoriteUsers.push(username);
saveFavoriteUsers();
showNotification(`Добавлен в избранное: @${username}`, 'success');
} else {
showNotification(`@${username} уже в избранном`, 'error');
}
} else if (displayMode === "blocked" && inputValue.startsWith("@")) {
const username = inputValue.slice(1);
if (!blockedUsers.includes(username)) {
blockedUsers.push(username);
saveBlockedUsers();
showNotification(`Заблокирован: @${username}`, 'success');
} else {
showNotification(`@${username} уже заблокирован`, 'error');
}
} else if (displayMode === "keywords" && !hiddenKeywords.includes(inputValue)) {
hiddenKeywords.push(inputValue);
saveKeywords();
showNotification(`Добавлено ключевое слово: ${inputValue}`, 'success');
} else {
showNotification(`Ключевое слово уже существует: ${inputValue}`, 'error');
}
keywordInput.value = "";
updateKeywordList();
updateCounters();
debouncedHidePosts();
}
});
toggleNonVerifiedBtn.textContent = `Hide non-verified accounts: ${hideNonVerifiedAccounts ? "Turn OFF" : "Turn ON"}`;
toggleNonVerifiedBtn.addEventListener("click", () => {
hideNonVerifiedAccounts = !hideNonVerifiedAccounts;
localStorage.setItem("hideNonVerifiedAccounts", JSON.stringify(hideNonVerifiedAccounts));
toggleNonVerifiedBtn.textContent = `Hide non-verified accounts: ${hideNonVerifiedAccounts ? "Turn OFF" : "Turn ON"}`;
toggleNonVerifiedBtn.classList.toggle('active', hideNonVerifiedAccounts);
hidePosts();
});
clearKeywordsBtn.addEventListener("click", () => {
if (confirm("Are you sure you want to clear the list?")) {
if (displayMode === "favorites") {
favoriteUsers = [];
saveFavoriteUsers();
} else if (displayMode === "blocked") {
blockedUsers = [];
saveBlockedUsers();
// NEW: Очистка GIF-списка, если режим keywords
} else if (displayMode === "keywords") {
hiddenKeywords = [];
gifBlacklist = [];
unblockedKeywords = [];
saveKeywords();
saveGifBlacklist();
saveUnblockedKeywords();
}
updateKeywordList();
updateCounters();
hidePosts();
}
});
// NEW: Модифицированный экспорт с поддержкой GIF
exportKeywordsBtn.addEventListener("click", () => {
let data, baseName, nameWord;
if (displayMode === "favorites") {
data = favoriteUsers;
baseName = "favorite_users";
nameWord = "users";
} else if (displayMode === "blocked") {
data = blockedUsers;
baseName = "blocked_users";
nameWord = "users";
} else {
data = { keywords: hiddenKeywords, gif: gifBlacklist }; // NEW: Экспорт как объект с GIF
baseName = "hidden_keywords_and_gif";
nameWord = "items";
}
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(data))}`;
const count = displayMode === "keywords" ? hiddenKeywords.length + gifBlacklist.length : data.length;
const fileName = `${baseName}_${count}_${nameWord}.json`;
const downloadAnchor = document.createElement("a");
downloadAnchor.setAttribute("href", dataStr);
downloadAnchor.setAttribute("download", fileName);
document.body.appendChild(downloadAnchor);
downloadAnchor.click();
document.body.removeChild(downloadAnchor);
});
// NEW: Модифицированный импорт с поддержкой GIF
importKeywordsBtn.addEventListener("click", () => {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.addEventListener("change", (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = () => {
try {
const importedData = JSON.parse(reader.result);
if (displayMode === "favorites" || displayMode === "blocked") {
const list = Array.isArray(importedData) ? importedData : [];
if (displayMode === "favorites") {
favoriteUsers = [...new Set([...favoriteUsers, ...list.map(u => u.startsWith("@") ? u.slice(1) : u)])];
saveFavoriteUsers();
} else {
blockedUsers = [...new Set([...blockedUsers, ...list.map(u => u.startsWith("@") ? u.slice(1) : u)])];
saveBlockedUsers();
}
} else if (displayMode === "keywords") {
if (Array.isArray(importedData)) {
hiddenKeywords = [...new Set([...hiddenKeywords, ...importedData])];
} else if (typeof importedData === 'object') {
hiddenKeywords = [...new Set([...hiddenKeywords, ...(importedData.keywords || [])])];
gifBlacklist = [...new Set([...gifBlacklist, ...(importedData.gif || [])])];
saveGifBlacklist();
}
saveKeywords();
}
updateKeywordList();
updateCounters();
hidePosts();
} catch (e) {
alert("Error reading the file.");
}
};
reader.readAsText(file);
});
input.click();
});
toggleBlockBtn.addEventListener("click", () => {
if (displayMode === "keywords") {
if (hiddenKeywords.length > 0 || gifBlacklist.length > 0) {
unblockedKeywords = [...hiddenKeywords, ...gifBlacklist.map(k => `gif:${k}`)]; // NEW: Сохраняем GIF с префиксом
hiddenKeywords = [];
gifBlacklist = [];
toggleBlockBtn.textContent = "Block All";
} else {
hiddenKeywords = unblockedKeywords.filter(k => !k.startsWith("gif:"));
gifBlacklist = unblockedKeywords.filter(k => k.startsWith("gif:")).map(k => k.slice(4));
unblockedKeywords = [];
toggleBlockBtn.textContent = "Unblock All";
}
saveKeywords();
saveGifBlacklist();
saveUnblockedKeywords();
updateKeywordList();
updateCounters();
hidePosts();
}
});
toggleBlockBtn.textContent = hiddenKeywords.length > 0 || gifBlacklist.length > 0 ? "Unblock All" : "Block All";
toggleVerifiedBtn.addEventListener("click", () => {
hideVerifiedAccounts = !hideVerifiedAccounts;
toggleVerifiedBtn.textContent = `Hide verified accounts: ${hideVerifiedAccounts ? "Turn OFF" : "Turn ON"}`;
toggleVerifiedBtn.classList.toggle('active', hideVerifiedAccounts);
hidePosts();
});
toggleFavoriteUsersBtn.addEventListener("click", () => {
displayMode = displayMode === "favorites" ? "keywords" : "favorites";
toggleFavoriteUsersBtn.textContent = displayMode === "favorites" ? "Keywords" : "Favorite Users";
toggleBlockedUsersBtn.textContent = "Blocked Users";
keywordInput.placeholder = displayMode === "favorites" || displayMode === "blocked" ? "Enter @username" : "Enter the word or gif:keyword"; // NEW: Уточнение плейсхолдера
toggleFavoriteUsersBtn.classList.toggle('active', displayMode === "favorites");
toggleBlockedUsersBtn.classList.remove('active');
updateKeywordList();
updateCounters();
});
toggleBlockedUsersBtn.addEventListener("click", () => {
displayMode = displayMode === "blocked" ? "keywords" : "blocked";
toggleBlockedUsersBtn.textContent = displayMode === "blocked" ? "Keywords" : "Blocked Users";
toggleFavoriteUsersBtn.textContent = "Favorite Users";
keywordInput.placeholder = displayMode === "favorites" || displayMode === "blocked" ? "Enter @username" : "Enter the word or gif:keyword"; // NEW: Уточнение плейсхолдера
toggleBlockedUsersBtn.classList.toggle('active', displayMode === "blocked");
toggleFavoriteUsersBtn.classList.remove('active');
updateKeywordList();
updateCounters();
});
openLanguagePopupBtn.addEventListener("click", () => {
const panelRect = panel.getBoundingClientRect();
languagePopup.style.top = `${panelRect.top - 320}px`;
languagePopup.style.left = `${panelRect.right - 10}px`;
languagePopup.style.display = "block";
});
// ========== Обновление списка ключевых слов, избранных или заблокированных пользователей ========== //
function updateKeywordList() {
const list = document.getElementById("keywordList");
const label = document.getElementById("listLabel");
const searchQuery = searchInput.value.trim().toLowerCase();
list.innerHTML = ""; // Оставляем, чтобы очистить при обновлении
if (displayMode === "favorites") {
label.textContent = "Favorite Users";
const tempData = favoriteUsers.filter(user => user.toLowerCase().includes(searchQuery)).map(user => ({ type: 'user', value: user }));
filteredData = tempData; // Используем полный массив без обрезки
if (filteredData.length === 0) {
list.innerHTML = searchQuery ? "<li>No matches found</li>" : "<li>Нет</li>";
} else {
renderVisibleItems(list);
}
} else if (displayMode === "blocked") {
label.textContent = "Blocked Users";
const tempData = blockedUsers.filter(user => user.toLowerCase().includes(searchQuery)).map(user => ({ type: 'user', value: user }));
filteredData = tempData; // Используем полный массив без обрезки
if (filteredData.length === 0) {
list.innerHTML = searchQuery ? "<li>No matches found</li>" : "<li>Нет</li>";
} else {
renderVisibleItems(list);
}
} else {
label.textContent = "List of Keywords and GIF";
const allItems = [
...hiddenKeywords.map(k => ({ type: 'keyword', value: k })),
...gifBlacklist.map(k => ({ type: 'gif', value: `gif:${k}` }))
];
const tempData = allItems.filter(item => item.value.toLowerCase().includes(searchQuery));
filteredData = tempData; // Используем полный массив без обрезки
if (filteredData.length === 0) {
list.innerHTML = searchQuery ? "<li>No matches found</li>" : "<li>Нет</li>";
} else {
renderVisibleItems(list);
}
}
if (filteredData.length > 0) {
list.removeEventListener('scroll', handleScroll);
list.addEventListener('scroll', handleScroll);
}
}
function renderVisibleItems(container) {
const scrollTop = container.scrollTop;
const startIndex = Math.floor(scrollTop / itemHeight);
const visibleCount = Math.ceil(viewportHeight / itemHeight) + 2; // Видимые + буфер (2 сверху/снизу)
const endIndex = Math.min(startIndex + visibleCount, filteredData.length);
container.innerHTML = "";
for (let i = startIndex; i < endIndex; i++) {
const item = filteredData[i];
const listItem = document.createElement("li");
listItem.style.position = 'absolute';
listItem.style.top = `${i * itemHeight}px`;
listItem.style.width = '100%';
listItem.textContent = item.value;
const deleteButton = document.createElement("button");
deleteButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
shape-rendering="geometricPrecision"
text-rendering="geometricPrecision"
image-rendering="optimizeQuality"
fill-rule="evenodd"
clip-rule="evenodd"
viewBox="0 0 456 511.82">
<path fill=" #FD3B3B"
d="M48.42 140.13h361.99c17.36 0 29.82 9.78 28.08 28.17l-30.73 317.1c-1.23 13.36-8.99 26.42-25.3 26.42H76.34c-13.63-.73-23.74-9.75-25.09-24.14L20.79 168.99c-1.74-18.38 9.75-28.86 27.63-28.86zM24.49 38.15h136.47V28.1c0-15.94 10.2-28.1 27.02-28.1h81.28c17.3 0 27.65 11.77 27.65 28.01v10.14h138.66c.57 0 1.11.07 1.68.13 10.23.93 18.15 9.02 18.69 19.22.03.79.06 1.39.06 2.17v42.76c0 5.99-4.73 10.89-10.62 11.19-.54 0-1.09.03-1.63.03H11.22c-5.92 0-10.77-4.6-11.19-10.38 0-.72-.03-1.47-.03-2.23v-39.5c0-10.93 4.21-20.71 16.82-23.02 2.53-.45 5.09-.37 7.67-.37zm83.78 208.38c-.51-10.17 8.21-18.83 19.53-19.31 11.31-.49 20.94 7.4 21.45 17.57l8.7 160.62c.51 10.18-8.22 18.84-19.53 19.32-11.32.48-20.94-7.4-21.46-17.57l-8.69-160.63zm201.7-1.74c.51-10.17 10.14-18.06 21.45-17.57 11.32.48 20.04 9.14 19.53 19.31l-8.66 160.63c-.52 10.17-10.14 18.05-21.46 17.57-11.31-.48-20.04-9.14-19.53-19.32l8.67-160.62zm-102.94.87c0-10.23 9.23-18.53 20.58-18.53 11.34 0 20.58 8.3 20.58 18.53v160.63c0 10.23-9.24 18.53-20.58 18.53-11.35 0-20.58-8.3-20.58-18.53V245.66z"/>
<style xmlns="http://www.w3.org/1999/xhtml"
id="mh1i307m.ol">
::selection {
background: #d0fefe4f !important;
color: #bee4e0 !important;
}
::-moz-selection {
background: #d0fefe4f !important;
color: #bee4e0 !important;
}
</style></svg>
`;
deleteButton.style.border = "none";
deleteButton.style.borderRadius = "3px";
deleteButton.style.cursor = "pointer";
deleteButton.addEventListener("click", () => {
if (item.type === 'gif') {
const gifKeyword = item.value.slice(4);
gifBlacklist.splice(gifBlacklist.indexOf(gifKeyword), 1);
saveGifBlacklist();
showNotification(`Удалено из GIF: ${gifKeyword}`, 'success');
} else if (item.type === 'user' && displayMode === 'favorites') {
favoriteUsers.splice(favoriteUsers.indexOf(item.value), 1);
saveFavoriteUsers();
showNotification(`Removed @${item.value} from favorites`, 'success');
} else if (item.type === 'user' && displayMode === 'blocked') {
blockedUsers.splice(blockedUsers.indexOf(item.value), 1);
saveBlockedUsers();
showNotification(`Unblocked @${item.value}`, 'success');
} else {
hiddenKeywords.splice(hiddenKeywords.indexOf(item.value), 1);
saveKeywords();
showNotification(`Удалено ключевое слово: ${item.value}`, 'success');
}
updateKeywordList();
updateCounters();
hidePosts();
});
listItem.appendChild(deleteButton);
container.appendChild(listItem);
}
// Ограничиваем высоту до 10 элементов
container.style.height = `${filteredData.length * itemHeight}px`; // Полная виртуальная высота
}
// обработчик скролла
function handleScroll() {
renderVisibleItems(this);
}
// ========== Создание кнопки избранного в профиле ========== //
function addFavoriteButtonToProfile() {
const checkProfileLoaded = setInterval(() => {
const buttonContainer = document.querySelector('.css-175oi2r.r-obd0qt.r-18u37iz.r-1w6e6rj.r-1h0z5md.r-dnmrzs') ||
document.querySelector('[data-testid="userActions"]');
const username = getUsernameFromProfile();
if (buttonContainer && username && !buttonContainer.querySelector('#favoriteUserButton')) {
clearInterval(checkProfileLoaded);
const isFavorite = favoriteUsers.includes(username);
const favoriteButton = document.createElement('button');
favoriteButton.id = 'favoriteUserButton';
favoriteButton.setAttribute('aria-label', isFavorite ? 'Удалить из избранного' : 'Добавить в избранное');
favoriteButton.setAttribute('role', 'button');
favoriteButton.type = 'button';
favoriteButton.className = 'css-175oi2r r-sdzlij r-1phboty r-rs99b7 r-lrvibr r-6gpygo r-1wron08 r-2yi16 r-1qi8awa r-1loqt21 r-o7ynqc r-6416eg r-1ny4l3l css-175oi2r r-18u37iz r-1wtj0ep';
favoriteButton.style.borderColor = 'rgb(83, 100, 113)';
favoriteButton.style.backgroundColor = 'rgba(0, 0, 0, 0)';
const buttonContent = document.createElement('div');
buttonContent.dir = 'ltr';
buttonContent.className = 'css-146c3p1 r-bcqeeo r-qvutc0 r-1qd0xha r-q4m81j r-a023e6 r-rjixqe r-b88u0q r-1awozwy r-6koalj r-18u37iz r-16y2uox r-1777fci';
buttonContent.style.color = 'rgb(239, 243, 244)';
const icon = document.createElement('img');
icon.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHAAAABwCAYAAADG4PRLAAALW0lEQVR4nO2dC5BXVR3Hv7soBCMi8XBhBdrlsa5aUq5UauYz02p6P8bH1NSUAT0hzEqbJvBRPiMz85EzZjD2EKLyMQqFI72MQhLZXQRBJEYzoJxFJGSbk98/c7397+937r3n3Md/72dmZ2DPveece7977vmd3znnd1BRUZEjTcGid+/e3QhadAA4B8BUAEMA7AOwFcADAP5YgPqlZtiwYQeyaCQB3wngmwBeL1yzBcC3AHw/w3o5p9EEPBTA3QBOj3HPRgDvANDjsV7eCArYXMYHCNAG4ImY4hkmA+hOcF/hKLOAhwF4CMCYFHk8COA4h3XKnDILeCuAIxzkc4eDPHKjrAK+HcD7HeV1FICLHOWVOWUVcK7FNf8E8HsAT1tc+zkABzuoV+aUUUAzxjtDueYSAKMBnABgAoCPAHhJuL4VwJcc1zMTyiig1vou40+QuwCcp9w3u4ytsGwCmr7vNCHdfC7nR6QZEZcL95pWOCdl/TKnbAJqL/hmAC8K6QuV+00rPChBvXKjTAKeyZ8odgH4rpLHMgB/EtJNfznLXZX9UyYBNSPjeoqocbmS/lk31c2Gsgho+r23CelGuO9Y5vULpRVO4bCiFJRFQK1VLLRsfTWuVNJnx8grV8og4CkA3iOk/ytG66uxBMCfhfRpAD4TM89cKIOA2os0fd+OBPmGx4pxyy0ERRfwZMXn+byF5RnFUqUVdpTBIi26gForuIE+z6RcpdxX+L6wyAKeBOCDQnofgOtSlvETAI8K6Wam4sKUZXilyAJqprz5dP7DQTnfVtILPS4s6poYszDpL0L6Pi6LeMpReevY2qK4AMCdjspKTRnWxGgTrNc4FA8W3pmLHZbllCK2wGMBrBHS99Nb8qTjcrtpeUZhpqMWOS4zEUVvgV9R0q/yIB6EaagaWr1yoWgt8DhlbPYSW99mT+WvB3CkkP7RIiyCCrbAPOe+mrmus4PTOGMBfEK551qP4oF9oSSQsXynA9gEYDuADQB6AezxWCeRLFpgK/+qTctpATAx8P+xMfLp5z2bfFQyQA99obY8TxG7uXT/GX7ie1jXfa4r6KMFtnAzyWS2plb+u5P/dsG1GYgHOsa/F+P64fz011sgvJPC9nKDzTY+Qw+t6P60lY3bAifTP2nEagcwjoJN8mwQ9bHM7R7LCLKG1rBPtgda7GaK+gcAf9XKjNsCm7gS7EJ+wvLg8xmKZ/g015T6ZBx/whhBf0yrWJ3j1FrgWzl3NtLzw0jMceDzTIJZAXdvDuUGMUbdD8O/tB0Hmhb32xzFM4bAiTmJZ7gPwGsAPJxT+YbbtJV0US3wgozHOy/y07GNfcBd3HlUFI7mO3kjrWhjPR+SYd2Mw/3Ltf9oGzyncHzjGmNOP0uRNtCB3M39fVu4NKIsDKXxNpVDjk4OjVrZrw318Bzv5rJIVcCfA3hfwkL6OcXzNHfBGvP5cVpYRqTn0j1DKRjOVjqVMxxH8t+tHG4lXb7fU/MSSQJ28oVr7KRIm3l9bSC7ia1sf+Pqk4rDOOTq5E8Hv3gTLJ0aZoL7Z9IwQttzZ1rXudzZWhGfXfyptwrAtNbFAF4n5PohLiH5Te0XYSv0FKVKp1XieeNxGkmSt6kLwIzgL8ICtgk3G5P2seI9d0Oxhwu1omjh5/YAYQEl68mHZVoR7z2/KqxRWMC9ws2unNIVMuOF1L1hjcICSvvJjfEyqnr53vmYUICx8P8e/EVYQMltNIq+wbIHByoytwN4s1C/dWELNizGUuXhjmfAuMEN9+ry5w6l9YFOlvuCvwgLaOajfqdk0kURhynXVdizmL5WCbOB56daHwjLoDfT2ZRHVCKlZhnDoGhcXM9fXE/AVQC+bpHhFK4gy9Ir32jcA+BdFs9k9nDcUi8hyiCZbxlT04i4lj6+ingYg/BsiztWAvhwVKJkUc6ynExtoyN7gsW1FS+zgjP+Gvdr7k1tSDCHEW41Dmef2F4JJNLEAAunWlz7axuRbcZ0pvNcYHHdSK7mirOmciAxiDbD8RbPvJQhpFVsB+WXWho2w/kXVon4SgZx6PUGi2vNUOG9thnH8arMt9zgYYYWq5X9dgOJIXwfNpGBF3HOz5q4brErLYPgHMI+8YQBK9vLjKCVbrNI+HaLiIr/RxK/ptngMdPiuoPoWz05QRmNQM2ws+lObgLw8STPnNQxbQr8pMV1TRzHDDQRx9JgmWRx7ULLBlGXNDMLt8b4q1lpaTo3AuPY59kEZL+G2wYSk3ZqKM53ewUP22hk2rjsxEa8y1yEeXYxt7coxjrSXwF4i4Myi8ho+pFfbVG3BYzrnRpXk7NLbAeeXBzViJPCN0fsNgrzNY6rneDyRVq5frhK2cYAKhOnWw6+L7IIaRIL1y3hfiUwa41EJnOBkUKC1ZhnEZstNj4+ZQ8wzpnEjAabvZAi6YNB8672UbCvvmiVhWFj01+UgRFccBvFNwDc6Os5fBoT2pE3TUp6WWhWnsVHUKID+BRQmxtME+ezSOxS9rJ7nZnxKWCnkPaM42B1edKvnAQq7TZKjU8BpbNs1yvL+MuGFDT2aJ/PklcLlB64jHQLdW73udTEl4CTOGCPYq2ncvNii1Ku9C5S4VNAiSd8PVBOaM8j7btMRV4CNooBU2OTEkXRW4QrXwIeI6RttfjklJG/CXX2FnfNl4CvFdIecxGlr4BIAh7jKzarDwGbFQFtwpiUEWks2OLLkPEhYLsyI+3VtZQjWr/uxSPjQ8CJSnoWQVvzYIMS4Eh7L4nwIaBmMjfqJ3SbMqCXHBuJ8SGg5PvrbVALtMY6Ic2LJZq1gI3a+mpILsIOxnlxig/TVvL7rfdQXhgzO/4B7gIyG09f4ItdxvNzfTrRJQNtFFchOA2Y5FrAFiVQjc/P56mc+a53cIcZ1pzPebu59cIYO0KzsCe6FtD1J3Sa8kfR67g8ULg1XDgsnboCtsjbaHBoIT2S0MMI+1E47wddCyi50PY67gPNdq2HKVzcFzOeq8o3MhKuK3ZkPbnrWkDpRfZyJj4t07gG1WweOTFlXu3cDbvW4d4NaapM8lAlwrWAUgUlE9uGIxgQx/yFn+O22v+r9woGOepKmZf0lTHutENT5v8KXArYrKz1TNr/jWQMza2WAXHSYOKUPcJwVtJZghKSp2m4YuTFxqWAY5S4z3EncQczzMmOhKdJb+Qp2KsS3HsWvSr3JJiM1axMaQ1pbFwKeLASkV09RibAJTwV7AsJ6tHHGDdTeIjVSfzkJjm27my2qDtjhNr8t5I+JEE9InEpYJ9iQtsEPZhNoecnjIh4KVdKh6NM3cuWdC4j7sflPB6ZcKNFkD/NUElSfiQuBdwZDkYaYi73jdfjfFqoNyQMoHc1Ayss4CmfUSzm/r2ZMb8INWayhV0utCRp399+jkGd4doKlUJVjuaA+yzGTRnCLVlmHu1HMQ+DrPEDCjdPaf1hbqJxNI/H/sRhEMOt9PE4nFH0cXbx+WYIea1xLaDrEzyNH3K5xXV7+MeTNHDsEh4R92zC+4OYd3BF8GyimPSzZQ2yuO2LAK5PW2Ht6J20mHP33uQiozos5+ZQH7P6Q/kpnuUhb9CabrMwclR8H0Oe9C9ZYjWDzJ7hcUnGCzSixjDclWu+6kK8MD4EfIhRDl3wKA+h7MpwOf5zDHdl1rbe7SjPW9hfO8fXssLrUh6c/yRDkkzP8RzBp3iW1FGW/XoUxmn+KV+V9Lm5xcRVOzPmPsDtDP7dTi9IEVjPT/cMRhy05T+MBeA1HoDvcB8Pcuw3R3Fmr6ZxMp5ejyLyCI0zE5Hil+wz67GB49GxbH1e8WGFRjGY4YOP5WB6P1vnan4my7Zau4NBiw7nEKKPrXVlzDFpbIJWaEVFRUVFRUVFRUVFRcUAAsB/ATtpI67MT344AAAAAElFTkSuQmCC';
icon.style.width = '25px';
icon.style.height = '25px';
icon.style.filter = isFavorite ? 'hue-rotate(300deg) saturate(2)' : 'none';
icon.setAttribute('aria-hidden', 'true');
icon.className = 'r-4qtqp9 r-yyyyoo r-dnmrzs r-bnwqim r-lrvibr r-m6rgpd r-z80fyv r-19wmn03';
buttonContent.appendChild(icon);
favoriteButton.appendChild(buttonContent);
favoriteButton.addEventListener('click', () => {
const currentlyFavorite = favoriteUsers.includes(username);
if (currentlyFavorite) {
favoriteUsers = favoriteUsers.filter(user => user !== username);
favoriteButton.setAttribute('aria-label', 'Добавить в избранное');
icon.style.filter = 'none';
} else {
favoriteUsers.push(username);
favoriteButton.setAttribute('aria-label', 'Удалить из избранного');
icon.style.filter = 'hue-rotate(300deg) saturate(2)';
}
saveFavoriteUsers();
updateKeywordList();
updateCounters();
debouncedHidePosts();
});
buttonContainer.insertBefore(favoriteButton, buttonContainer.firstChild);
console.log('Favorite button added for:', username);
}
}, 500);
setTimeout(() => clearInterval(checkProfileLoaded), 5000);
}
// ========== Автоматическое скрытие панели при открытии фото ========== //
function isPhotoViewerOpen() {
const currentUrl = window.location.href;
const isPhotoOpen = /\/photo\/\d+$/.test(currentUrl);
const photoModal = document.querySelector('div[aria-label="Image"]') || document.querySelector('div[data-testid="imageViewer"]');
return isPhotoOpen || !!photoModal;
}
function updateToggleButtonVisibility() {
toggleButton.style.display = isGrokPage() ? "none" : "flex";
}
function toggleElementsVisibility() {
const isPhotoOpen = isPhotoViewerOpen();
if (isPhotoOpen || isGrokPage()) {
panel.style.display = "none";
toggleButton.style.display = "none";
} else {
if (isPanelVisible) {
panel.style.display = "block";
panel.style.height = "710px";
}
toggleButton.style.display = "flex";
}
}
// ========== Наблюдение за изменениями DOM ========== //
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
// Игнорируем изменения, связанные с .gif-control-container
if (mutation.target.classList?.contains('gif-control-container')) {
continue;
}
debouncedHidePosts();
break; // Обрабатываем только первое релевантное изменение
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false // Игнорируем изменения атрибутов, если не нужны
});
const profileObserver = new MutationObserver(() => {
if (window.location.href.includes('x.com/') && !window.location.href.includes('/status/')) {
addFavoriteButtonToProfile();
}
});
profileObserver.observe(document.body, { childList: true, subtree: true });
toggleElementsVisibility();
window.addEventListener("popstate", () => {
updateToggleButtonVisibility();
toggleElementsVisibility();
});
const urlObserver = new MutationObserver(() => {
updateToggleButtonVisibility();
toggleElementsVisibility();
});
urlObserver.observe(document.body, { childList: true, subtree: true });
document.addEventListener("click", (event) => {
if (event.target.closest('div[aria-label="Close"]') || event.target.closest('div[data-testid="imageViewer-close"]')) {
setTimeout(toggleElementsVisibility, 100);
}
});
// ========== Инициализация ========== //
updateKeywordList();
updateCounters();
hidePosts();
// Добавим инициализацию активного состояния при загрузке
document.addEventListener('DOMContentLoaded', () => {
toggleVerifiedBtn.classList.toggle('active', hideVerifiedAccounts);
toggleNonVerifiedBtn.classList.toggle('active', hideNonVerifiedAccounts);
toggleFavoriteUsersBtn.classList.toggle('active', displayMode === "favorites");
toggleBlockedUsersBtn.classList.toggle('active', displayMode === "blocked");
});
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址