// ==UserScript==
// @name PanelControl - 1.8 Filtration Posts by language by Keywords - XFilter Twitter X-com (c) tapeavion
// @namespace http://tampermonkey.net/
// @version 1.8
// @description Hide posts by keywords with the dashboard and hide posts from verified accounts
// @author gullampis810
// @match https://x.com/*
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// @icon https://www.pinclipart.com/picdir/big/450-4507608_twitter-circle-clipart.png
// ==/UserScript==
(function () {
'use strict';
// ===== Настройки и инициализация ===== //
const STORAGE_KEY = 'hiddenKeywords';
let hiddenKeywords = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
let hideVerifiedAccounts = true; // Скрывать подтвержденные аккаунты
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帿ŨÆ]/,
};
let activeLanguageFilters = {}; // Словарь активных языков
// ===== Сохранение в localStorage ===== //
function saveKeywords() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(hiddenKeywords));
}
// ===== Функция для обновления отображения по языкам ===== //
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; // Возвращаем true, если текст на этом языке
}
}
return false;
}
// ===== Функция Скрытия постов ===== //
function hidePosts() {
document.querySelectorAll('article').forEach((article) => {
const textContent = article.innerText.toLowerCase();
const isVerifiedAccount = hideVerifiedAccounts && article.querySelector('[data-testid="icon-verified"]');
if (hiddenKeywords.some(keyword => textContent.includes(keyword.toLowerCase())) ||
isTextInLanguage(textContent) ||
isVerifiedAccount) {
article.style.display = 'none';
}
});
}
// ===== Создание панели управления ===== //
function createControlPanel() {
const panel = document.createElement('div');
panel.style.position = 'fixed';
panel.style.bottom = '0px';
panel.style.right = '440px';
panel.style.width = '450px';
panel.style.height = '545px'; // Увеличена высота панели
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.innerHTML = `
<h3 style="margin: 0; font-size: 16px;">Hiding Control</h3>
<input id="keywordInput" type="text" placeholder="Enter the word" style="width: calc(100% - 95px); height: 30px; padding: 5px; margin: 10px 0; border-radius: 5px; border: none;">
<button id="addKeyword" style="position: relative; width: 80px; padding: 8px; margin-bottom: 5px; background: #203142; color: white; border: none; border-radius: 5px; height: 45px; left: 5px;">
Add it
</button>
<button id="clearKeywords" style="width: 75px; padding: 8px; margin-bottom: 5px; background: #203142; color: white; border: none; border-radius: 5px; height: 40px;">Clear all</button>
<button id="exportKeywords" style="width: 60px; padding: 8px; margin-bottom: 5px; background: #203142; color: white; border: none; border-radius: 5px; height: 40px;">Export</button>
<button id="importKeywords" style="width: 60px; padding: 8px; background: #203142; color: white; border: none; border-radius: 5px; margin-bottom: 10px; height: 40px;">Import</button>
<button id="toggleVerifiedPosts" style="width: 242px; padding: 8px; margin-bottom: 5px; background: #203142; color: white; border: none; border-radius: 5px; height: 40px; font-size: 13px;
font-weight: bold;">
Hide verified accounts: Click to Disable
</button>
<label style="display: block; margin: 10px 0;"></label>
<ul id="keywordList" style="list-style: inside; padding: 0; margin-top: 10px; font-size: 14px; color: #fff;"></ul>
<div style="margin-top: 20px; font-size: 14px; color: #fff;">
<p>Select Language to Hide Posts:</p>
<div id="languageButtons" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
${Object.keys(languageFilters).map(language => `
<label style="display: flex; align-items: center; color: #fff;">
<input type="checkbox" id="lang-${language}" style="margin-right: 8px;">
${language.charAt(0).toUpperCase() + language.slice(1)}
</label>
`).join('')}
</div>
</div>
`;
document.body.appendChild(panel);
// Обработчик для кнопок языков
Object.keys(languageFilters).forEach(language => {
const checkbox = document.getElementById(`lang-${language}`);
checkbox.addEventListener('change', () => {
updateLanguageFilter(language); // Включение/выключение фильтра для языка
});
});
// Стили для подсветки
const style = document.createElement('style');
style.textContent = `
button {
transition: box-shadow 0.3s, transform 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);
}
`;
document.head.appendChild(style);
// ======== Кнопка iOS-переключатель Panel FilterX ========= //
// Проверяем сохраненное состояние переключателя в localStorage
let isSwitchOn = localStorage.getItem('isSwitchOff') === 'true'; // Начальное состояние переключателя из localStorage
// Создание элементов панели
const toggleButton = document.createElement('div');
toggleButton.style.position = 'fixed';
toggleButton.style.top = '94%';
toggleButton.style.right = '90px';
toggleButton.style.width = '192px';
toggleButton.style.display = 'flex';
toggleButton.style.alignItems = 'center';
toggleButton.style.gap = '10px';
toggleButton.style.zIndex = '1';
toggleButton.style.background = '#15202b';
toggleButton.style.border = '4px solid #6c7e8e';
toggleButton.style.borderRadius = '18px';
toggleButton.style.boxSizing = 'border-box';
// Создаем label для переключателя
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 = '#0d1319';
toggleLabel.style.position = 'relative';
toggleLabel.style.cursor = 'pointer';
toggleLabel.style.transition = 'background-color 0.3s';
// Создаем кружок переключателя
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';
// Устанавливаем начальное состояние фона
toggleLabel.style.backgroundColor = isSwitchOn ? '#425364' : '#0d1319';
// Функция для изменения состояния переключателя
function toggleSwitchState() {
isSwitchOn = !isSwitchOn;
localStorage.setItem('isSwitchOn', isSwitchOn.toString()); // Сохраняем состояние в localStorage (как строку)
// Обновляем стиль переключателя
toggleSwitch.style.left = isSwitchOn ? 'calc(100% - 23px)' : '2px';
toggleLabel.style.backgroundColor = isSwitchOn ? '#425364' : '#0d1319';
}
// Добавляем обработчик на клик по переключателю
toggleLabel.addEventListener('click', toggleSwitchState);
// Добавляем элементы на страницу
toggleLabel.appendChild(toggleSwitch);
toggleButton.appendChild(toggleLabel);
document.body.appendChild(toggleButton);
// Добавление текста к переключателю
const toggleText = document.createElement('span');
toggleText.style.position = 'relative';
toggleText.style.right = '5px';
toggleText.textContent = 'Panel FilterX';
toggleText.style.color = '#6c7e8e';
toggleText.style.fontFamily = 'Arial, sans-serif';
toggleText.style.fontSize = '16px';
toggleText.style.marginLeft = '10px';
toggleText.style.fontWeight = 'bold';
toggleButton.appendChild(toggleText);
//====================== Управление высотой панели =======================//
let isPanelVisible = localStorage.getItem('panelVisible') === 'false'; // По умолчанию скрыта, если в localStorage не сохранено другое значение
function togglePanel() {
if (isPanelVisible) {
panel.style.height = '0px'; // Сворачиваем панель
setTimeout(() => {
panel.style.display = 'none'; // Скрываем панель после анимации
}, 300);
} else {
panel.style.display = 'block'; // Показываем панель
setTimeout(() => {
panel.style.height = '545px'; // Разворачиваем панель
}, 10);
}
isPanelVisible = !isPanelVisible; // Переключаем состояние
localStorage.setItem('panelVisible', isPanelVisible.toString()); // Сохраняем состояние
}
toggleButton.addEventListener('click', togglePanel);
// При загрузке восстанавливаем состояние панели
if (isPanelVisible) {
panel.style.height = '545px'; // Разворачиваем панель
panel.style.display = 'block';
} else {
panel.style.height = '0px'; // Сворачиваем панель
panel.style.display = 'none';
}
// ===== Обработчики событий ===== //
document.getElementById('addKeyword').addEventListener('click', () => {
const input = document.getElementById('keywordInput');
const keyword = input.value.trim();
if (keyword && !hiddenKeywords.includes(keyword)) {
hiddenKeywords.push(keyword);
saveKeywords();
updateKeywordList();
hidePosts();
input.value = '';
}
});
document.getElementById('clearKeywords').addEventListener('click', () => {
if (confirm('Are you sure you want to clear the list?')) {
hiddenKeywords = [];
saveKeywords();
updateKeywordList();
hidePosts();
}
});
document.getElementById('exportKeywords').addEventListener('click', () => {
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(hiddenKeywords))}`;
const downloadAnchor = document.createElement('a');
downloadAnchor.setAttribute('href', dataStr);
downloadAnchor.setAttribute('download', 'hidden_keywords.json');
document.body.appendChild(downloadAnchor);
downloadAnchor.click();
document.body.removeChild(downloadAnchor);
});
document.getElementById('importKeywords').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 importedKeywords = JSON.parse(reader.result);
if (Array.isArray(importedKeywords)) {
hiddenKeywords = [...new Set([...hiddenKeywords, ...importedKeywords])];
saveKeywords();
updateKeywordList();
hidePosts();
} else {
alert('Incorrect file format.');
}
} catch (e) {
alert('Error reading the file.');
}
};
reader.readAsText(file);
});
input.click();
});
// ===== Кнопка для включения/выключения скрытия подтвержденных аккаунтов ===== //
document.getElementById('toggleVerifiedPosts').addEventListener('click', () => {
hideVerifiedAccounts = !hideVerifiedAccounts;
document.getElementById('toggleVerifiedPosts').textContent = `Hide verified accounts: ${hideVerifiedAccounts ? 'Turn OFF' : 'Turn ON'}`;
hidePosts(); // Перепроверка всех постов с новыми настройками
});
}
// ===== Обновление списка ключевых слов ===== //
function updateKeywordList() {
const list = document.getElementById('keywordList');
list.style.maxHeight = '150px';
list.style.overflowY = 'auto';
list.style.paddingRight = '10px';
list.style.border = '1px solid #ccc';
list.style.borderRadius = '5px';
list.style.backgroundColor = '#15202b';
list.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.3)';
list.innerHTML = '';
hiddenKeywords.forEach((keyword, index) => {
const listItem = document.createElement('li');
listItem.textContent = keyword;
listItem.style.marginBottom = '5px';
const deleteButton = document.createElement('button');
deleteButton.textContent = '❌';
deleteButton.style.marginLeft = '10px';
deleteButton.style.backgroundColor = '#f44336';
deleteButton.style.color = '#fff';
deleteButton.style.border = 'none';
deleteButton.style.borderRadius = '3px';
deleteButton.style.cursor = 'pointer';
deleteButton.addEventListener('click', () => {
hiddenKeywords.splice(index, 1);
saveKeywords();
updateKeywordList();
hidePosts();
});
listItem.appendChild(deleteButton);
list.appendChild(listItem);
});
if (hiddenKeywords.length === 0) {
list.textContent = 'Нет';
}
}
// ===== Инициализация ===== //
createControlPanel();
updateKeywordList(); // Обновление списка при загрузке страницы
hidePosts();
// ===== Наблюдатель для динамического контента ===== //
const observer = new MutationObserver(hidePosts);
observer.observe(document.body, { childList: true, subtree: true });
})();