Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save TwoXTwentyOne/8ceffec11bdb9cfe231ee1d41af5554a to your computer and use it in GitHub Desktop.
Save TwoXTwentyOne/8ceffec11bdb9cfe231ee1d41af5554a to your computer and use it in GitHub Desktop.
TamperMonkey - Unfollow Unverified Account
// ==UserScript==
// @name X (Twitter) - v1.18 + Verified-No-Followback + Start/Stop Toggle + Dark Mode (All Text White)
// @namespace http://tampermonkey.net/
// @version 1.18
// @description Contextual counters, S=Start, A=Abort, P=Pause, E=Export, ignore list, plus an option to unfollow verified if they don’t follow back, with Start/Stop toggles and Dark Mode (all text white)
// @match https://x.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// ------------------- DARK MODE GLOBALS & STYLE -------------------
// Load the dark mode setting from localStorage (default to false)
let darkModeEnabled = localStorage.getItem('my_dark_mode') === "true";
// Inject dark mode CSS styles
function initDarkModeStyles() {
const style = document.createElement("style");
style.textContent = `
/* Dark Mode Styles: all text will be white */
.dark-mode {
background-color: #333 !important;
color: #fff !important;
border: 1px solid #555 !important;
}
.dark-mode * {
color: #fff !important;
}
.dark-mode button {
background-color: #555 !important;
color: #fff !important;
}
.dark-mode textarea {
background-color: #555 !important;
color: #fff !important;
border: 1px solid #777 !important;
}
.dark-mode a {
color: #66aaff !important;
}
`;
document.head.appendChild(style);
}
initDarkModeStyles();
// ------------------- STORAGE KEYS -------------------
const panelId = '__sedgwickz__unfollow_id';
const storageKeyIgnoreList = '__my_ignore_list';
const storageKeyUnfollowLimit = '__my_unfollow_limit';
const storageKeyRemoveLimit = '__my_remove_limit';
const storageKeyVerifiedNoFollowback = '__unfollow_verified_no_followback';
// ------------------- GLOBALS -------------------
let ignoreList = loadIgnoreList();
let isPaused = false;
let abortNow = false;
let unfollowedCount = 0;
let removedFollowersCount = 0;
let userUnfollowLimit = loadLimit(storageKeyUnfollowLimit, 50000);
let userRemoveLimit = loadLimit(storageKeyRemoveLimit, 50000);
// Whether to unfollow verified who do not follow me
let unfollowVerifiedNoFollowback = loadBooleanSetting(storageKeyVerifiedNoFollowback, false);
const seenHandles = new Set();
const allUsersArr = [];
const recentProcessed = [];
let recentProcessedDiv = null;
let countDiv = null;
let ignoreTextArea = null;
let isRunning = false;
// We’ll store references to the main “Start”/“Stop” buttons so we can toggle their text.
let unfollowButton = null;
let removeFollowersButton = null;
// --------------- MAIN ENTRY ---------------
function start() {
if (isFollowingPage() || isFollowersPage()) {
createPanel();
} else {
removePanel();
}
}
function removePanel() {
const panel = document.getElementById(panelId);
if (panel) panel.remove();
}
// Refresh the page every 25 min
setTimeout(() => {
location.reload();
}, 1500000);
// SPA transitions
if (window?.navigation?.addEventListener) {
window.navigation.addEventListener("navigate", () => {
setTimeout(start, 200);
});
}
if (window.onurlchange === null) {
window.addEventListener('urlchange', () => {
setTimeout(start, 200);
});
}
// --------------- KEYBOARD SHORTCUTS ---------------
document.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase();
if (k === 'p') {
isPaused = !isPaused;
notify(isPaused ? "Paused (P key)" : "Resumed (P key)");
updatePauseButtonText(isPaused);
}
else if (k === 'a') {
abortNow = true;
notify("Abort requested (A key). Loops will halt!");
}
else if (k === 'e') {
if (isFollowingPage() || isFollowersPage()) {
notify("Keyboard Export triggered...");
exportChronologicalList();
}
}
else if (k === 's') {
if (!isRunning && (isFollowingPage() || isFollowersPage())) {
notify("Starting main routine (S key)...");
startMainRoutine();
}
}
});
function updatePauseButtonText(isPaused) {
const panel = document.getElementById(panelId);
if (!panel) return;
const pauseBtn = panel.querySelector('button[data-role="pauseBtn"]');
if (!pauseBtn) return;
pauseBtn.innerText = isPaused ? "Resume" : "Pause";
}
start(); // On script load
// --------------- CREATE UI PANEL ---------------
function createPanel() {
removePanel();
abortNow = false;
isRunning = false;
const container = document.createElement('div');
container.id = panelId;
// Base (light mode) styles:
applyStyles(container, {
position: 'fixed',
zIndex: 9999999,
padding: '10px',
top: '0',
right: '0',
background: '#e0e0e0',
border: '1px solid #ccc',
borderRadius: '5px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
gap: '6px',
width: '280px'
});
// Apply dark mode if enabled:
if (darkModeEnabled) {
container.classList.add("dark-mode");
}
document.body.appendChild(container);
// ~~~~~~~~~~~~~~~~~~~~~ BUTTONS ~~~~~~~~~~~~~~~~~~~~~
if (isFollowingPage()) {
// Green button (unfollow)
unfollowButton = createButton("Remove Unverified Accounts - Start", '#28a745');
container.appendChild(unfollowButton);
unfollowButton.addEventListener('click', () => {
if (!isRunning) {
// Not running => start
startMainRoutine();
unfollowButton.innerText = "Stop Unfollowing";
} else {
// Already running => request abort
abortNow = true;
notify("Stop requested from button!");
unfollowButton.innerText = "Stopping...";
}
});
}
if (isFollowersPage()) {
// Red button (remove followers)
removeFollowersButton = createButton("Remove Unverified Followers", '#dc3545');
container.appendChild(removeFollowersButton);
removeFollowersButton.addEventListener('click', () => {
if (!isRunning) {
// Not running => start
startMainRoutine();
removeFollowersButton.innerText = "Stop Removing";
} else {
// Already running => request abort
abortNow = true;
notify("Stop requested from button!");
removeFollowersButton.innerText = "Stopping...";
}
});
}
// Export
if (isFollowingPage() || isFollowersPage()) {
const exportButton = createButton("Export Chronological (Bottom->Top)", '#007bff');
container.appendChild(exportButton);
exportButton.addEventListener('click', async () => {
exportButton.innerText = "Gathering users from bottom...";
await exportChronologicalList();
exportButton.innerText = "Export Chronological (Bottom->Top)";
});
}
// Pause/Resume
const pauseButton = createButton("Pause", '#ffc107');
pauseButton.setAttribute('data-role', 'pauseBtn');
container.appendChild(pauseButton);
pauseButton.addEventListener('click', () => {
isPaused = !isPaused;
pauseButton.innerText = isPaused ? "Resume" : "Pause";
});
// Dark Mode Toggle Button
const darkModeToggle = createButton(darkModeEnabled ? "Light Mode" : "Dark Mode", '#6c757d');
container.appendChild(darkModeToggle);
darkModeToggle.addEventListener("click", () => {
darkModeEnabled = !darkModeEnabled;
localStorage.setItem('my_dark_mode', darkModeEnabled ? "true" : "false");
container.classList.toggle("dark-mode", darkModeEnabled);
darkModeToggle.innerText = darkModeEnabled ? "Light Mode" : "Dark Mode";
});
// ~~~~~~~~~~~~~~~~~~~~~ Count Display ~~~~~~~~~~~~~~~~~~~~~
countDiv = document.createElement('div');
updateCountDisplay(countDiv);
container.appendChild(countDiv);
// ~~~~~~~~~~~~~~~~~~~~~ Limits & Checkboxes ~~~~~~~~~~~~~~~~~~~~~
if (isFollowingPage()) {
// Unfollow limit
const limitLabel = document.createElement('div');
limitLabel.innerText = "Unfollow Limit (0=Unlimited):";
applyStyles(limitLabel, {
fontWeight: 'bold',
marginTop: '5px',
color: '#444'
});
container.appendChild(limitLabel);
const inputUnfollowLimit = document.createElement('input');
inputUnfollowLimit.type = 'number';
inputUnfollowLimit.min = '0';
inputUnfollowLimit.value = (userUnfollowLimit === Number.MAX_SAFE_INTEGER) ? '0' : String(userUnfollowLimit);
inputUnfollowLimit.style.width = '120px';
container.appendChild(inputUnfollowLimit);
const saveLimitBtn = createButton("Save Unfollow Limit", '#6c757d');
container.appendChild(saveLimitBtn);
saveLimitBtn.addEventListener('click', () => {
const newLimit = parseInt(inputUnfollowLimit.value, 10) || 0;
userUnfollowLimit = (newLimit === 0) ? Number.MAX_SAFE_INTEGER : newLimit;
localStorage.setItem(storageKeyUnfollowLimit, String(newLimit));
notify("Unfollow Limit saved!");
updateCountDisplay(countDiv);
});
// Checkbox: Unfollow Verified Who Don’t Follow Me
const verifyDiv = document.createElement('div');
verifyDiv.style.marginTop = '6px';
verifyDiv.style.display = 'flex';
verifyDiv.style.alignItems = 'center';
const verifyCheckbox = document.createElement('input');
verifyCheckbox.type = 'checkbox';
verifyCheckbox.checked = unfollowVerifiedNoFollowback;
verifyCheckbox.id = '__verify_checkbox_id';
verifyDiv.appendChild(verifyCheckbox);
const verifyLabel = document.createElement('label');
verifyLabel.htmlFor = '__verify_checkbox_id';
verifyLabel.innerText = "Unfollow Verified Who Don’t Follow Me";
verifyLabel.style.marginLeft = '6px';
verifyLabel.style.color = '#222';
verifyDiv.appendChild(verifyLabel);
container.appendChild(verifyDiv);
verifyCheckbox.addEventListener('change', () => {
unfollowVerifiedNoFollowback = verifyCheckbox.checked;
localStorage.setItem(storageKeyVerifiedNoFollowback, String(unfollowVerifiedNoFollowback));
notify(
"Setting updated: " +
(unfollowVerifiedNoFollowback ? "Will" : "Won't") +
" remove verified who don't follow back."
);
});
}
if (isFollowersPage()) {
// Remove limit
const limitLabel = document.createElement('div');
limitLabel.innerText = "Remove Limit (0=Unlimited):";
applyStyles(limitLabel, {
fontWeight: 'bold',
marginTop: '5px',
color: '#444'
});
container.appendChild(limitLabel);
const inputRemoveLimit = document.createElement('input');
inputRemoveLimit.type = 'number';
inputRemoveLimit.min = '0';
inputRemoveLimit.value = (userRemoveLimit === Number.MAX_SAFE_INTEGER) ? '0' : String(userRemoveLimit);
inputRemoveLimit.style.width = '120px';
container.appendChild(inputRemoveLimit);
const saveLimitBtn = createButton("Save Remove Limit", '#6c757d');
container.appendChild(saveLimitBtn);
saveLimitBtn.addEventListener('click', () => {
const newLimit = parseInt(inputRemoveLimit.value, 10) || 0;
userRemoveLimit = (newLimit === 0) ? Number.MAX_SAFE_INTEGER : newLimit;
localStorage.setItem(storageKeyRemoveLimit, String(newLimit));
notify("Remove Limit saved!");
updateCountDisplay(countDiv);
});
}
// ~~~~~~~~~~~~~~~~~~~~~ Ignore List ~~~~~~~~~~~~~~~~~~~~~
const ignoreLabel = document.createElement('div');
ignoreLabel.innerText = "Ignore List (User won't be removed):";
applyStyles(ignoreLabel, {
fontWeight: 'bold',
marginTop: '10px',
color: '#444'
});
container.appendChild(ignoreLabel);
ignoreTextArea = document.createElement('textarea');
ignoreTextArea.rows = 4;
ignoreTextArea.cols = 22;
ignoreTextArea.value = ignoreList.join("\n");
container.appendChild(ignoreTextArea);
const saveIgnoreButton = createButton("Save Ignore List", '#6c757d');
container.appendChild(saveIgnoreButton);
saveIgnoreButton.addEventListener('click', () => {
const lines = ignoreTextArea.value
.split("\n")
.map(s => s.trim())
.filter(Boolean);
ignoreList = lines.map(h => h.toLowerCase());
saveIgnoreList(ignoreList);
notify("Ignore List saved!");
});
// ~~~~~~~~~~~~~~~~~~~~~ Recently Processed ~~~~~~~~~~~~~~~~~~~~~
const recentLabel = document.createElement('div');
recentLabel.innerText = "Recently Processed (Last 25):";
applyStyles(recentLabel, {
fontWeight: 'bold',
marginTop: '10px',
color: '#444'
});
container.appendChild(recentLabel);
recentProcessedDiv = document.createElement('div');
applyStyles(recentProcessedDiv, {
maxHeight: '120px',
overflowY: 'auto',
border: '1px solid #ccc',
padding: '3px',
width: '100%'
});
container.appendChild(recentProcessedDiv);
updateRecentProcessedUI();
// ~~~~~~~~~~~~~~~~~~~~~ Help/Feedback ~~~~~~~~~~~~~~~~~~~~~
const helpDiv = document.createElement('div');
helpDiv.innerHTML = `
<div style="font-weight:bold; margin-top:10px; color:#444;">Keyboard Shortcuts:</div>
<ul style="margin:0; padding-left:18px; font-size:12px; color:#333;">
<li><strong>S</strong>: Start Process</li>
<li><strong>A</strong>: Abort Immediately</li>
<li><strong>P</strong>: Pause/Resume</li>
<li><strong>E</strong>: Export Chronological</li>
</ul>
`;
container.appendChild(helpDiv);
const descDiv = document.createElement('div');
descDiv.innerHTML = `
<div style='font-weight: bold; color: #555;'>Feedback &amp; Support</div>
<div><a href='https://x.com/eric_periard' target='_blank' style='color: darkblue;'>@eric_periard</a></div>`;
applyStyles(descDiv, { textAlign: 'center' });
container.appendChild(descDiv);
applyHoverEffects(container);
}
// ~~~~~~~~~~~~~~~~~~~~~ MAIN ROUTINE ---------------------
async function startMainRoutine() {
if (isRunning) return;
isRunning = true;
abortNow = false;
if (isFollowingPage()) {
notify("Starting unfollow routine...");
document.documentElement.scrollTo(0, 0);
await sleep(2000);
while (!abortNow && unfollowedCount < userUnfollowLimit) {
await unFollow();
}
if (abortNow) {
notify("Aborted unfollow routine!");
} else {
notify("Unfollow process completed!");
}
// reset button label
if (unfollowButton) {
unfollowButton.innerText = "Remove Unverified Accounts - Start";
}
}
else if (isFollowersPage()) {
notify("Starting remove-follower routine...");
document.documentElement.scrollTo(0, 0);
await sleep(2000);
while (!abortNow && removedFollowersCount < userRemoveLimit) {
await removeUnverifiedFollowers();
}
if (abortNow) {
notify("Aborted remove-follower routine!");
} else {
notify("Remove-follower process completed!");
}
// reset button label
if (removeFollowersButton) {
removeFollowersButton.innerText = "Remove Unverified Followers";
}
}
isRunning = false;
}
// ~~~~~~~~~~~~~~~~~~~~~ RECORD PROCESSED ---------------------
function recordProcessedAction(handle, actionType) {
const entry = `${actionType} @${handle}`;
recentProcessed.unshift(entry);
if (recentProcessed.length > 25) recentProcessed.pop();
updateRecentProcessedUI();
}
function updateRecentProcessedUI() {
if (!recentProcessedDiv) return;
const lines = recentProcessed
.map(item => `<div style="font-size: 12px; color: #333;">${item}</div>`)
.join("");
recentProcessedDiv.innerHTML = lines || "<div style='font-size:12px; color:#999;'>No recent actions</div>";
}
// ~~~~~~~~~~~~~~~~~~~~~ UNFOLLOW LOGIC ---------------------
async function unFollow() {
const container = document.querySelector('[aria-label*="Following"]');
if (!container) {
console.log("No main container found for 'Following'");
return;
}
const buttons = container.querySelectorAll("button[role='button']");
for (const item of buttons) {
if (unfollowedCount >= userUnfollowLimit || abortNow) break;
while (isPaused) {
await sleep(500);
if (abortNow) break;
}
if (abortNow) break;
if (item.innerText === 'Following') {
const accountElement = item.closest('[data-testid="UserCell"]');
if (!accountElement) continue;
const userHandle = getHandleFromUserCell(accountElement);
if (!userHandle) continue;
// skip if in ignore list
if (ignoreList.includes(userHandle.toLowerCase())) {
continue;
}
const verifiedIcon = accountElement.querySelector('svg[aria-label="Verified account"]');
const isVerified = !!verifiedIcon;
if (isVerified) {
if (!unfollowVerifiedNoFollowback) {
// user didn't check the box => skip verified
continue;
}
// user DID check => unfollow verified only if "does not follow me"
if (doesFollowMe(accountElement)) {
// they DO follow me => skip
continue;
}
}
// else if unverified => just proceed
// highlight + random delay
accountElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
highlightElement(accountElement);
await sleep(randomDelay(2000, 5000));
if (abortNow) break;
item.click();
await sleep(1000);
if (abortNow) break;
const confirmButton = getConfirmButton();
if (confirmButton) confirmButton.click();
unfollowedCount++;
updateCountDisplay(countDiv);
recordProcessedAction(userHandle, "Unfollowed");
}
}
document.documentElement.scrollTo(0, 999999999);
await sleep(2000);
}
function doesFollowMe(accountElement) {
const label = [...accountElement.querySelectorAll('span, div')]
.find(el => el.innerText && el.innerText.includes("Follows you"));
return !!label;
}
function getConfirmButton() {
return [...document.querySelectorAll("button[role='button']")]
.find(item => item.innerText === 'Unfollow');
}
// ~~~~~~~~~~~~~~~~~~~~~ REMOVE FOLLOWERS ---------------------
async function removeUnverifiedFollowers() {
const container = document.querySelector('[aria-label*="Followers"]');
if (!container) {
console.log("No main container found for 'Followers'");
return;
}
const allUserCells = container.querySelectorAll('[data-testid="UserCell"]');
for (const cell of allUserCells) {
if (removedFollowersCount >= userRemoveLimit || abortNow) break;
while (isPaused) {
await sleep(500);
if (abortNow) break;
}
if (abortNow) break;
const userHandle = getHandleFromUserCell(cell);
if (!userHandle) continue;
if (ignoreList.includes(userHandle.toLowerCase())) {
continue;
}
const isVerified = !!cell.querySelector('svg[aria-label="Verified account"]');
if (!isVerified) {
// highlight + random delay
cell.scrollIntoView({ behavior: 'smooth', block: 'center' });
highlightElement(cell);
await sleep(randomDelay(1500, 3000));
if (abortNow) break;
const menuButton = findThreeDotMenu(cell);
if (!menuButton) continue;
menuButton.click();
await sleep(1200);
if (abortNow) break;
const removeBtn = [...document.querySelectorAll('span, div')]
.find(el => el.innerText === 'Remove this follower');
if (!removeBtn) {
closeAnyMenu();
continue;
}
removeBtn.click();
await sleep(1000);
if (abortNow) break;
const confirmRemoveButton = [...document.querySelectorAll('span, div')]
.find(el => el.innerText === 'Remove');
if (confirmRemoveButton) confirmRemoveButton.click();
removedFollowersCount++;
updateCountDisplay(countDiv);
recordProcessedAction(userHandle, "Removed");
}
}
document.documentElement.scrollTo(0, 999999999);
await sleep(2000);
}
function findThreeDotMenu(cell) {
let menuButton = cell.querySelector('[aria-label="More"]');
if (!menuButton) {
menuButton = cell.querySelector('[data-testid="UserCellOverflowButton"]');
}
if (!menuButton) {
menuButton = [...cell.querySelectorAll('span, div, button')]
.find(el => el.innerText === '…' || el.innerText === '...');
}
return menuButton;
}
function closeAnyMenu() {
const overlay = document.querySelector('[data-testid="sheetDialog"]');
if (overlay) overlay.click();
}
// ~~~~~~~~~~~~~~~~~~~~~ HIGHLIGHT ---------------------
function highlightElement(el) {
el.style.transition = "background-color 0.5s ease";
el.style.backgroundColor = "yellow";
setTimeout(() => {
el.style.backgroundColor = "";
}, 2000);
}
// ~~~~~~~~~~~~~~~~~~~~~ EXPORT LOGIC ---------------------
async function exportChronologicalList() {
const MAX_SCROLL_ITERATIONS = 200;
const MAX_STABLE_SCROLLS = 5;
let previousCount = 0;
let stableScrolls = 0;
for (let i = 0; i < MAX_SCROLL_ITERATIONS; i++) {
while (isPaused) {
await sleep(500);
if (abortNow) return;
}
if (abortNow) return;
gatherVisibleUsers();
window.scrollTo(0, document.body.scrollHeight);
await sleep(2000);
const currentCount = seenHandles.size;
if (currentCount === previousCount) {
stableScrolls++;
} else {
stableScrolls = 0;
}
previousCount = currentCount;
if (stableScrolls >= MAX_STABLE_SCROLLS) break;
}
gatherVisibleUsers();
const reversed = [...allUsersArr].reverse();
const csv = generateCSV(reversed);
downloadCSV(csv);
notify(`Exported ${reversed.length} users (oldest at the top)`);
}
function gatherVisibleUsers() {
let container = isFollowersPage()
? document.querySelector('[aria-label*="Followers"]')
: document.querySelector('[aria-label*="Following"]');
if (!container) {
console.log("No main container found for export");
return;
}
const cells = container.querySelectorAll('[data-testid="UserCell"]');
for (const cell of cells) {
const handle = getHandleFromUserCell(cell);
if (!handle) continue;
if (!seenHandles.has(handle)) {
seenHandles.add(handle);
const displayName = getDisplayNameFromCell(cell) || "";
const verified = !!cell.querySelector('svg[aria-label="Verified account"]');
allUsersArr.push({ handle, displayName, verified });
}
}
}
function getDisplayNameFromCell(cell) {
const nameEl = cell.querySelector('[data-testid="User-Name"] span');
if (nameEl && nameEl.innerText) {
return nameEl.innerText.trim();
}
const fallbackEl = cell.querySelector('span');
return fallbackEl ? fallbackEl.innerText.trim() : null;
}
function generateCSV(dataArray) {
const header = ["Handle", "DisplayName", "Verified"];
const rows = dataArray.map(user => [
user.handle,
user.displayName.replace(/,/g, ""),
user.verified ? "Yes" : "No"
]);
return [header.join(","), ...rows.map(r => r.join(","))].join("\n");
}
function downloadCSV(csv) {
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "x_users_chronological.csv";
a.click();
URL.revokeObjectURL(url);
}
// ~~~~~~~~~~~~~~~~~~~~~ HELPERS ---------------------
function getHandleFromUserCell(cell) {
const link = cell.querySelector('a[href*="/"]');
if (!link) return null;
const urlPath = link.getAttribute('href') || '';
const user = urlPath.split('/').pop().replace('@', '').toLowerCase();
return user;
}
function isFollowingPage() {
return location.pathname.endsWith('/following');
}
function isFollowersPage() {
return location.pathname.endsWith('/followers');
}
function sleep(ms) {
return new Promise(res => setTimeout(res, ms));
}
function randomDelay(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function createButton(text, bgColor) {
const button = document.createElement('button');
button.innerText = text;
applyStyles(button, {
backgroundColor: bgColor,
color: '#fff',
border: 'none',
borderRadius: '5px',
padding: '8px 14px',
cursor: 'pointer',
fontWeight: 'bold',
marginRight: '5px'
});
return button;
}
function applyStyles(element, styles) {
Object.assign(element.style, styles);
}
function applyHoverEffects(container) {
const style = document.createElement('style');
style.textContent = `
#${panelId} button:hover {
opacity: 0.8;
}
#${panelId} button:active {
transform: scale(0.95);
}
`;
document.head.appendChild(style);
}
function updateCountDisplay(div) {
if (!div) return;
if (isFollowingPage()) {
const remaining = (userUnfollowLimit === Number.MAX_SAFE_INTEGER)
? 'Unlimited'
: Math.max(0, userUnfollowLimit - unfollowedCount);
div.innerHTML = `
<div style="color: red">
Unfollowed: ${unfollowedCount} | Remaining: ${remaining}
</div>
`;
}
else if (isFollowersPage()) {
const remaining = (userRemoveLimit === Number.MAX_SAFE_INTEGER)
? 'Unlimited'
: Math.max(0, userRemoveLimit - removedFollowersCount);
div.innerHTML = `
<div style="color: purple">
Removed: ${removedFollowersCount} | Remaining: ${remaining}
</div>
`;
} else {
div.innerHTML = "";
}
}
function notify(message) {
if (Notification.permission === "granted") {
new Notification(message);
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then(permission => {
if (permission === "granted") new Notification(message);
});
}
console.log(message); // fallback
}
function loadIgnoreList() {
try {
const data = localStorage.getItem(storageKeyIgnoreList);
if (!data) return [];
return JSON.parse(data);
} catch (e) {
return [];
}
}
function saveIgnoreList(list) {
localStorage.setItem(storageKeyIgnoreList, JSON.stringify(list));
}
function loadLimit(key, defaultValue) {
const data = localStorage.getItem(key);
if (!data) return defaultValue;
let val = parseInt(data, 10);
if (isNaN(val)) {
return defaultValue;
}
if (val === 0) {
return Number.MAX_SAFE_INTEGER;
}
return val;
}
function loadBooleanSetting(key, defaultVal) {
const data = localStorage.getItem(key);
if (!data) return defaultVal;
return data === "true";
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment