diff --git a/shared-ip-manager.userscript.js b/shared-ip-manager.userscript.js new file mode 100644 index 0000000..b7d670d --- /dev/null +++ b/shared-ip-manager.userscript.js @@ -0,0 +1,517 @@ +// ==UserScript== +// @name Shared IP Manager +// @namespace http://tampermonkey.net/ +// @version 1.1.0 +// @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 `; + + // 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 `; + + // Usage header + const usageHeader = document.createElement('th'); + usageHeader.style.cssText = `padding: 10px 12px; text-align: left; font-weight: 600; color: #959595; white-space: nowrap;`; + usageHeader.textContent = 'Usage'; + + // 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 = ` + + + ${isBanned ? '🚫 ' : ''}${username} + + ${isBanned ? 'BANNED' : ''} + + ${joinDate} + ${messages} + + ${flagEmoji} ${countryCode} + + + ${providerInfo.name} + + + ${typeInfo.replace('Type: ', '')} + + + ${usageCount} + + `; + + // 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() : ''; + } + + console.log('Comparing:', aValue, 'vs', bValue); // Debug log + + 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'; + }); + + // 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); + } + }); + +})();