From 2733e86c8fff9e1871a1f9bd4a5ad65a694b99fc Mon Sep 17 00:00:00 2001 From: Ryahn Date: Wed, 10 Sep 2025 00:18:12 +0000 Subject: [PATCH] Add shared-ip-manager.userscript.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Shared IP Manager - Changelog ## Version 1.1.0 - Feature Enhancement Release ### �� New Features #### Advanced Search & Filtering - **Real-time search** across all table columns (username, country, provider, type, etc.) - **Banned user filter toggle** - quickly view only banned users - **Combined filtering** - search within filtered results - **User count display** with banned user statistics #### Column Sorting - **Sortable Username column** - alphabetical A-Z / Z-A - **Sortable Type column** - group similar types together - **Visual sort indicators** - ↕ (unsorted), ↑ (ascending), ↓ (descending) - **Click to toggle** between ascending/descending order #### Banned User Detection & Styling - **Automatic detection** of banned users from data structure - **Visual indicators**: - 🚫 Ban icon before username - Red strikethrough text - "BANNED" badge with red styling - **Maintains functionality** - banned user links still work #### Enhanced UI/UX - **Dark theme styling** matching overlay design - **Responsive table** with horizontal/vertical scrolling - **Hover effects** on sortable headers and buttons - **Professional layout** with proper spacing and typography ### 🔧 Technical Improvements #### Performance - **Efficient DOM manipulation** - direct element creation vs innerHTML - **Optimized filtering** - real-time updates without lag - **Memory management** - proper event listener cleanup #### Code Quality - **Error handling** - null checks and graceful fallbacks - **Debug logging** - comprehensive console output for troubleshooting - **Modular functions** - clean, maintainable code structure ### 🐛 Bug Fixes - **Fixed null reference errors** in event listener setup - **Resolved sorting issues** with proper DOM element access - **Fixed table overflow** - proper scrolling implementation ### 📊 User Interface #### Layout - **Flexbox search bar** with filter button and count display - **Sticky table headers** that remain visible while scrolling - **Consistent spacing** and visual hierarchy #### Visual Design - **Color scheme**: Dark backgrounds (#101113, #131313) with red accents (#dc2626) - **Typography**: System font stack for better readability - **Interactive elements**: Smooth transitions and hover states ### 🎯 Use Cases #### For Moderators - **Quick banned user review** - filter to see only banned users - **Search specific users** - find users by name, country, or provider - **Sort by activity** - organize by username or connection type #### For Analysis - **Data visualization** - clean table format vs overlay - **Export-friendly** - structured data for further analysis - **Real-time filtering** - dynamic data exploration --- ## Version 1.0.0 - Initial Release ### Core Functionality - Transform shared IP overlay into searchable table format - Basic table structure with user data extraction - Initial dark theme implementation --- *This userscript enhances the shared IP management experience by providing powerful filtering, sorting, and visualization tools for better user moderation and data analysis.* --- shared-ip-manager.userscript.js | 517 ++++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 shared-ip-manager.userscript.js 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); + } + }); + +})();