555 lines
23 KiB
JavaScript
555 lines
23 KiB
JavaScript
// ==UserScript==
|
|
// @name Shared IP Manager
|
|
// @namespace http://tampermonkey.net/
|
|
// @version 1.1.2
|
|
// @description Transform shared IP overlay into searchable table format
|
|
// @author Ryahn
|
|
// @match *://*/*
|
|
// @grant none
|
|
// ==/UserScript==
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Function to extract country code from flag class
|
|
function getCountryCode(flagElement) {
|
|
const classes = flagElement.className;
|
|
const match = classes.match(/tck-provider-country-flag\s+(\w+)/);
|
|
return match ? match[1].toUpperCase() : '';
|
|
}
|
|
|
|
// Function to get country name from flag element
|
|
function getCountryName(flagElement) {
|
|
return flagElement.getAttribute('data-original-title') || '';
|
|
}
|
|
|
|
// Function to get provider info
|
|
function getProviderInfo(providerElement) {
|
|
return {
|
|
name: providerElement.textContent.trim(),
|
|
asn: providerElement.getAttribute('data-original-title') || ''
|
|
};
|
|
}
|
|
|
|
// Function to get type info
|
|
function getTypeInfo(typeElement) {
|
|
return typeElement.getAttribute('data-original-title') || '';
|
|
}
|
|
|
|
// Function to extract usage count
|
|
function getUsageCount(usageText) {
|
|
const match = usageText.match(/(\d+)\s+time/);
|
|
return match ? parseInt(match[1]) : 0;
|
|
}
|
|
|
|
// Function to create flag emoji from country code
|
|
function getFlagEmoji(countryCode) {
|
|
if (!countryCode) return '';
|
|
const codePoints = countryCode
|
|
.toLowerCase()
|
|
.split('')
|
|
.map(char => 127397 + char.charCodeAt());
|
|
return String.fromCodePoint(...codePoints);
|
|
}
|
|
|
|
// Function to create the table
|
|
function createTable() {
|
|
const overlay = document.querySelector('.overlay-content');
|
|
if (!overlay) return;
|
|
|
|
// Find all user entries
|
|
const userEntries = overlay.querySelectorAll('.block-row.block-row--separated');
|
|
|
|
if (userEntries.length === 0) return;
|
|
|
|
// Create table container
|
|
const tableContainer = document.createElement('div');
|
|
tableContainer.style.cssText = `
|
|
padding: 0;
|
|
background: transparent;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
`;
|
|
|
|
// Create search and filter container
|
|
const searchFilterContainer = document.createElement('div');
|
|
searchFilterContainer.style.cssText = `
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
align-items: center;
|
|
`;
|
|
|
|
// Create search input
|
|
const searchInput = document.createElement('input');
|
|
searchInput.type = 'text';
|
|
searchInput.placeholder = 'Search users, countries, providers...';
|
|
searchInput.style.cssText = `
|
|
flex: 1;
|
|
padding: 12px;
|
|
border: 1px solid #101113;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
box-sizing: border-box;
|
|
background: #101113;
|
|
color: #374151;
|
|
`;
|
|
|
|
// Create banned filter button
|
|
const bannedFilterButton = document.createElement('button');
|
|
bannedFilterButton.textContent = 'Show Banned Only';
|
|
bannedFilterButton.style.cssText = `
|
|
padding: 12px 16px;
|
|
border: 1px solid #dc2626;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
background: #dc262620;
|
|
color: #dc2626;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
white-space: nowrap;
|
|
`;
|
|
|
|
// Add hover effect for button
|
|
bannedFilterButton.addEventListener('mouseenter', function() {
|
|
this.style.backgroundColor = '#dc2626';
|
|
this.style.color = 'white';
|
|
});
|
|
bannedFilterButton.addEventListener('mouseleave', function() {
|
|
if (!this.classList.contains('active')) {
|
|
this.style.backgroundColor = '#dc262620';
|
|
this.style.color = '#dc2626';
|
|
}
|
|
});
|
|
|
|
// Create count display
|
|
const countDisplay = document.createElement('div');
|
|
countDisplay.style.cssText = `
|
|
color: #6b7280;
|
|
font-size: 12px;
|
|
margin-left: auto;
|
|
padding: 8px 12px;
|
|
background: #1a1a1a;
|
|
border-radius: 4px;
|
|
border: 1px solid #17191b;
|
|
`;
|
|
|
|
// Add button to container
|
|
searchFilterContainer.appendChild(searchInput);
|
|
searchFilterContainer.appendChild(bannedFilterButton);
|
|
searchFilterContainer.appendChild(countDisplay);
|
|
|
|
// Create table wrapper for scrolling
|
|
const tableWrapper = document.createElement('div');
|
|
tableWrapper.style.cssText = `
|
|
width: 100%;
|
|
overflow: auto;
|
|
max-height: calc(100vh - 150px);
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 6px;
|
|
`;
|
|
|
|
// Create table
|
|
const table = document.createElement('table');
|
|
table.style.cssText = `
|
|
width: 100%;
|
|
min-width: 800px;
|
|
border-collapse: collapse;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-size: 13px;
|
|
margin: 0;
|
|
`;
|
|
|
|
// Create table header
|
|
const thead = document.createElement('thead');
|
|
thead.style.cssText = `position: sticky; top: 0; z-index: 10;`;
|
|
|
|
const headerRow = document.createElement('tr');
|
|
headerRow.style.cssText = `background: #101113; border-bottom: 1px solid #101113; color: #959595;`;
|
|
|
|
// Username header (sortable)
|
|
const usernameHeader = document.createElement('th');
|
|
usernameHeader.id = 'sort-username';
|
|
usernameHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap; cursor: pointer; user-select: none;`;
|
|
usernameHeader.innerHTML = `Username <span id="username-sort-icon" style="margin-left: 5px;">↕</span>`;
|
|
|
|
// Join Date header
|
|
const joinDateHeader = document.createElement('th');
|
|
joinDateHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap;`;
|
|
joinDateHeader.textContent = 'Join Date';
|
|
|
|
// Messages header
|
|
const messagesHeader = document.createElement('th');
|
|
messagesHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap;`;
|
|
messagesHeader.textContent = 'Messages';
|
|
|
|
// Country header
|
|
const countryHeader = document.createElement('th');
|
|
countryHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap;`;
|
|
countryHeader.textContent = 'Country';
|
|
|
|
// Provider header
|
|
const providerHeader = document.createElement('th');
|
|
providerHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap;`;
|
|
providerHeader.textContent = 'Provider';
|
|
|
|
// Type header (sortable)
|
|
const typeHeader = document.createElement('th');
|
|
typeHeader.id = 'sort-type';
|
|
typeHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap; cursor: pointer; user-select: none;`;
|
|
typeHeader.innerHTML = `Type <span id="type-sort-icon" style="margin-left: 5px;">↕</span>`;
|
|
|
|
// Usage header (sortable)
|
|
const usageHeader = document.createElement('th');
|
|
usageHeader.id = 'sort-usage';
|
|
usageHeader.style.cssText = `padding: 10px 12px; text-align: left; font-weight: 600; color: #959595; white-space: nowrap; cursor: pointer; user-select: none;`;
|
|
usageHeader.innerHTML = `Usage <span id="usage-sort-icon" style="margin-left: 5px;">↕</span>`;
|
|
|
|
// Append all headers to row
|
|
headerRow.appendChild(usernameHeader);
|
|
headerRow.appendChild(joinDateHeader);
|
|
headerRow.appendChild(messagesHeader);
|
|
headerRow.appendChild(countryHeader);
|
|
headerRow.appendChild(providerHeader);
|
|
headerRow.appendChild(typeHeader);
|
|
headerRow.appendChild(usageHeader);
|
|
|
|
thead.appendChild(headerRow);
|
|
|
|
// Create table body
|
|
const tbody = document.createElement('tbody');
|
|
|
|
// Process each user entry
|
|
userEntries.forEach((entry, index) => {
|
|
const row = document.createElement('tr');
|
|
row.style.cssText = `
|
|
border-bottom: 1px solid #17191b;
|
|
transition: background-color 0.2s;
|
|
`;
|
|
row.style.backgroundColor = index % 2 === 0 ? '#131313' : '#131313';
|
|
|
|
// Username
|
|
const usernameLink = entry.querySelector('h3.contentRow-header a.username');
|
|
const username = usernameLink ? usernameLink.textContent.trim() : 'N/A';
|
|
const profileUrl = usernameLink ? usernameLink.href : '#';
|
|
|
|
// Check if user is banned
|
|
const usernameSpan = entry.querySelector('h3.contentRow-header a.username span');
|
|
const isBanned = usernameSpan && usernameSpan.classList.contains('username--banned');
|
|
|
|
// Join date
|
|
const joinDateElement = entry.querySelector('time.u-dt');
|
|
const joinDate = joinDateElement ? joinDateElement.textContent.trim() : 'N/A';
|
|
|
|
// Messages
|
|
const dtElements = entry.querySelectorAll('dt');
|
|
let messagesElement = null;
|
|
for (const dt of dtElements) {
|
|
if (dt.textContent.includes('Messages')) {
|
|
messagesElement = dt;
|
|
break;
|
|
}
|
|
}
|
|
const messages = messagesElement ?
|
|
(messagesElement.nextElementSibling ? messagesElement.nextElementSibling.textContent.trim() : '0') : '0';
|
|
|
|
// Country flag and name
|
|
const flagElement = entry.querySelector('.tck-provider-country-flag');
|
|
const countryCode = flagElement ? getCountryCode(flagElement) : '';
|
|
const countryName = flagElement ? getCountryName(flagElement) : '';
|
|
const flagEmoji = getFlagEmoji(countryCode);
|
|
|
|
// Provider info
|
|
const providerElement = entry.querySelector('.tck-provider-txt');
|
|
const providerInfo = providerElement ? getProviderInfo(providerElement) : { name: 'N/A', asn: '' };
|
|
|
|
// Type info
|
|
const typeElement = entry.querySelector('.tck-provider-type');
|
|
const typeInfo = typeElement ? getTypeInfo(typeElement) : 'N/A';
|
|
|
|
// Usage count
|
|
const liElements = entry.querySelectorAll('li');
|
|
let usageElement = null;
|
|
for (const li of liElements) {
|
|
if (li.textContent.includes('time')) {
|
|
usageElement = li;
|
|
break;
|
|
}
|
|
}
|
|
const usageText = usageElement ? usageElement.textContent.trim() : '0 time';
|
|
const usageCount = getUsageCount(usageText);
|
|
|
|
|
|
// Build row HTML
|
|
row.innerHTML = `
|
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap;">
|
|
<a href="${profileUrl}" target="_blank" style="color: ${isBanned ? '#dc2626' : '#2563eb'}; text-decoration: ${isBanned ? 'line-through' : 'none'}; font-weight: 500;">
|
|
${isBanned ? '🚫 ' : ''}${username}
|
|
</a>
|
|
${isBanned ? '<span style="color: #dc2626; margin-left: 8px; font-size: 11px; font-weight: 600; background: #dc262620; padding: 2px 6px; border-radius: 3px;">BANNED</span>' : ''}
|
|
</td>
|
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap; color: #6b7280;">${joinDate}</td>
|
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap; color: #6b7280;">${messages}</td>
|
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap;">
|
|
<span title="${countryName}" style="color: #374151;">${flagEmoji} ${countryCode}</span>
|
|
</td>
|
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${providerInfo.asn}">
|
|
<span style="color: #374151;">${providerInfo.name}</span>
|
|
</td>
|
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap;" title="${typeInfo}">
|
|
<span style="color: #6b7280;">${typeInfo.replace('Type: ', '')}</span>
|
|
</td>
|
|
<td style="padding: 10px 12px; text-align: center; white-space: nowrap;">
|
|
<span style="color: #374151; font-weight: 500;">${usageCount}</span>
|
|
</td>
|
|
`;
|
|
|
|
// Add hover effect
|
|
row.addEventListener('mouseenter', () => {
|
|
row.style.backgroundColor = '#131313';
|
|
});
|
|
row.addEventListener('mouseleave', () => {
|
|
row.style.backgroundColor = index % 2 === 0 ? '#131313' : '#131313';
|
|
});
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// Filter state
|
|
let showBannedOnly = false;
|
|
|
|
// Function to apply both search and filter
|
|
function applyFilters() {
|
|
const searchTerm = searchInput.value.toLowerCase();
|
|
const rows = tbody.querySelectorAll('tr');
|
|
let visibleCount = 0;
|
|
let bannedCount = 0;
|
|
|
|
rows.forEach(row => {
|
|
const text = row.textContent.toLowerCase();
|
|
const isBanned = row.querySelector('a[href*="/members/"]') &&
|
|
row.querySelector('a[href*="/members/"]').textContent.includes('🚫');
|
|
|
|
const matchesSearch = text.includes(searchTerm);
|
|
const matchesFilter = !showBannedOnly || isBanned;
|
|
|
|
const shouldShow = matchesSearch && matchesFilter;
|
|
row.style.display = shouldShow ? '' : 'none';
|
|
|
|
if (shouldShow) {
|
|
visibleCount++;
|
|
if (isBanned) bannedCount++;
|
|
}
|
|
});
|
|
|
|
// Update count display
|
|
if (showBannedOnly) {
|
|
countDisplay.textContent = `Showing ${bannedCount} banned users`;
|
|
} else {
|
|
countDisplay.textContent = `${visibleCount} users (${bannedCount} banned)`;
|
|
}
|
|
}
|
|
|
|
// Add search functionality
|
|
searchInput.addEventListener('input', applyFilters);
|
|
|
|
// Add banned filter functionality
|
|
bannedFilterButton.addEventListener('click', () => {
|
|
showBannedOnly = !showBannedOnly;
|
|
|
|
if (showBannedOnly) {
|
|
bannedFilterButton.textContent = 'Show All Users';
|
|
bannedFilterButton.classList.add('active');
|
|
bannedFilterButton.style.backgroundColor = '#dc2626';
|
|
bannedFilterButton.style.color = 'white';
|
|
} else {
|
|
bannedFilterButton.textContent = 'Show Banned Only';
|
|
bannedFilterButton.classList.remove('active');
|
|
bannedFilterButton.style.backgroundColor = '#dc262620';
|
|
bannedFilterButton.style.color = '#dc2626';
|
|
}
|
|
|
|
applyFilters();
|
|
});
|
|
|
|
// Initialize filters to set up count display
|
|
applyFilters();
|
|
|
|
// Assemble table
|
|
table.appendChild(thead);
|
|
table.appendChild(tbody);
|
|
|
|
// Add table to wrapper
|
|
tableWrapper.appendChild(table);
|
|
|
|
// Add elements to container
|
|
tableContainer.appendChild(searchFilterContainer);
|
|
tableContainer.appendChild(tableWrapper);
|
|
|
|
// Sorting functionality
|
|
let currentSort = { column: null, direction: 'asc' };
|
|
|
|
function sortTable(column, direction) {
|
|
console.log('Sorting by:', column, direction); // Debug log
|
|
|
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
console.log('Found rows:', rows.length); // Debug log
|
|
|
|
if (rows.length === 0) {
|
|
console.log('No rows to sort');
|
|
return;
|
|
}
|
|
|
|
rows.sort((a, b) => {
|
|
let aValue, bValue;
|
|
|
|
if (column === 'username') {
|
|
const aLink = a.querySelector('a');
|
|
const bLink = b.querySelector('a');
|
|
aValue = aLink ? aLink.textContent.trim().toLowerCase() : '';
|
|
bValue = bLink ? bLink.textContent.trim().toLowerCase() : '';
|
|
} else if (column === 'type') {
|
|
aValue = a.cells[5] ? a.cells[5].textContent.trim().toLowerCase() : '';
|
|
bValue = b.cells[5] ? b.cells[5].textContent.trim().toLowerCase() : '';
|
|
} else if (column === 'usage') {
|
|
// Parse usage as numbers for proper numerical sorting
|
|
aValue = parseFloat(a.cells[6] ? a.cells[6].textContent.trim() : '0') || 0;
|
|
bValue = parseFloat(b.cells[6] ? b.cells[6].textContent.trim() : '0') || 0;
|
|
}
|
|
|
|
console.log('Comparing:', aValue, 'vs', bValue); // Debug log
|
|
|
|
if (column === 'usage') {
|
|
// Numerical comparison for usage
|
|
if (direction === 'asc') {
|
|
return aValue - bValue;
|
|
} else {
|
|
return bValue - aValue;
|
|
}
|
|
} else {
|
|
// String comparison for username and type
|
|
if (direction === 'asc') {
|
|
return aValue.localeCompare(bValue);
|
|
} else {
|
|
return bValue.localeCompare(aValue);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Clear tbody and re-append sorted rows
|
|
tbody.innerHTML = '';
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
|
|
// Update sort icons
|
|
document.querySelectorAll('[id$="-sort-icon"]').forEach(icon => {
|
|
icon.textContent = '↕';
|
|
});
|
|
|
|
const sortIcon = document.getElementById(`${column}-sort-icon`);
|
|
if (sortIcon) {
|
|
sortIcon.textContent = direction === 'asc' ? '↑' : '↓';
|
|
}
|
|
|
|
console.log('Sorting completed'); // Debug log
|
|
}
|
|
|
|
// Add click event listeners for sortable columns
|
|
console.log('Adding click listener to username header');
|
|
usernameHeader.addEventListener('click', (e) => {
|
|
console.log('Username header clicked');
|
|
e.preventDefault();
|
|
if (currentSort.column === 'username') {
|
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
currentSort.column = 'username';
|
|
currentSort.direction = 'asc';
|
|
}
|
|
console.log('Current sort:', currentSort);
|
|
sortTable(currentSort.column, currentSort.direction);
|
|
});
|
|
|
|
// Add hover effects for username header
|
|
usernameHeader.addEventListener('mouseenter', function() {
|
|
this.style.backgroundColor = '#1a1a1a';
|
|
});
|
|
usernameHeader.addEventListener('mouseleave', function() {
|
|
this.style.backgroundColor = '#101113';
|
|
});
|
|
|
|
console.log('Adding click listener to type header');
|
|
typeHeader.addEventListener('click', (e) => {
|
|
console.log('Type header clicked');
|
|
e.preventDefault();
|
|
if (currentSort.column === 'type') {
|
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
currentSort.column = 'type';
|
|
currentSort.direction = 'asc';
|
|
}
|
|
console.log('Current sort:', currentSort);
|
|
sortTable(currentSort.column, currentSort.direction);
|
|
});
|
|
|
|
// Add hover effects for type header
|
|
typeHeader.addEventListener('mouseenter', function() {
|
|
this.style.backgroundColor = '#1a1a1a';
|
|
});
|
|
typeHeader.addEventListener('mouseleave', function() {
|
|
this.style.backgroundColor = '#101113';
|
|
});
|
|
|
|
console.log('Adding click listener to usage header');
|
|
usageHeader.addEventListener('click', (e) => {
|
|
console.log('Usage header clicked');
|
|
e.preventDefault();
|
|
if (currentSort.column === 'usage') {
|
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
currentSort.column = 'usage';
|
|
currentSort.direction = 'asc';
|
|
}
|
|
console.log('Current sort:', currentSort);
|
|
sortTable(currentSort.column, currentSort.direction);
|
|
});
|
|
|
|
// Add hover effects for usage header
|
|
usageHeader.addEventListener('mouseenter', function() {
|
|
this.style.backgroundColor = '#1a1a1a';
|
|
});
|
|
usageHeader.addEventListener('mouseleave', function() {
|
|
this.style.backgroundColor = '#101113';
|
|
});
|
|
|
|
// Replace overlay content
|
|
overlay.innerHTML = '';
|
|
overlay.appendChild(tableContainer);
|
|
}
|
|
|
|
// Wait for overlay to load and then transform it
|
|
function waitForOverlay() {
|
|
const overlay = document.querySelector('.overlay-content');
|
|
if (overlay) {
|
|
createTable();
|
|
} else {
|
|
// Check again in 100ms
|
|
setTimeout(waitForOverlay, 100);
|
|
}
|
|
}
|
|
|
|
// Start monitoring for overlay
|
|
waitForOverlay();
|
|
|
|
// Also listen for overlay events in case it loads dynamically
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('js-overlayClose') ||
|
|
e.target.closest('.js-overlayClose')) {
|
|
// Overlay is closing, wait for it to reopen
|
|
setTimeout(waitForOverlay, 500);
|
|
}
|
|
});
|
|
|
|
})();
|