commit b26d0aecee986a84b27c3d6f64a6ff0dc4b3350d Author: Chatter Date: Thu Jan 29 09:22:24 2026 -0500 first push diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2caece --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# FabSwingers Chat Enhancements + +A Tampermonkey userscript that adds useful features to the FabSwingers chat. + +## Features + +- **MaxDockedCams** - Override the limit on how many webcam feeds you can dock +- **Keep Alive** - Automatically sends a message to prevent being timed out for inactivity (default: 10 minutes) +- **Multi-Poke** - Adds a "10x" button next to each user's poke button to send 10 pokes rapidly +- **Draggable Buttons** - All control buttons can be dragged anywhere on screen and positions are saved + +## Installation + +### Step 1: Install Tampermonkey + +#### Firefox +1. Go to Tampermonkey for Firefox +2. Click "Add to Firefox" +3. Click "Add" when prompted for permissions + +#### Chrome +1. Go to Tampermonkey for Chrome +2. Click "Add to Chrome" +3. Click "Add extension" when prompted +4. **Important:** Enable userscripts using one of these methods: + + **Option A: Allow User Scripts (Chrome 138+)** + 1. Right-click the Tampermonkey icon in your toolbar + 2. Select "Manage extension" + 3. Toggle on "Allow User Scripts" + + **Option B: Enable Developer Mode** + 1. Go to `chrome://extensions` in your address bar + 2. Toggle on "Developer mode" in the top-right corner + + *Note: Tampermonkey 5.3+ requires one of these options enabled to run userscripts in Chrome-based browsers.* + +### Step 2: Install the Script + +**Method A: Direct Install (if Tampermonkey prompts automatically)** + +Click this link: **Install Script** + +If Tampermonkey opens automatically, click "Install" and you're done. + +**Method B: Manual Install (if the link just shows code)** + +1. Go to the script URL: https://git.upto.im/geekery/scripts/raw/branch/main/cams.user.js +2. Select all the code (Ctrl+A / Cmd+A) and copy it (Ctrl+C / Cmd+C) +3. Click the Tampermonkey icon in your browser toolbar +4. Select "Create a new script..." +5. Delete any existing code in the editor +6. Paste the copied code (Ctrl+V / Cmd+V) +7. Press Ctrl+S / Cmd+S to save, or click File → Save + +The script will automatically update when new versions are released. + +## Usage + +Once installed, visit the FabSwingers chat. You'll see three buttons in the top-right corner: + +| Button | Function | +|--------|----------| +| **10x ON/OFF** | Toggle multi-poke buttons on user list | +| **Keep Alive ON** | Shows status and interval; click to configure | +| **Set MaxDockedCams** | Click to set custom docked cam limit | + +### Moving Buttons + +All buttons are draggable. Click and drag to reposition them anywhere on screen. Positions are saved automatically. + +### Resetting Button Positions + +If buttons end up off-screen (e.g., after switching between monitors), click the small "R" button in the top-left corner to reset all buttons to their default positions. + +### Keep Alive Settings + +Click the Keep Alive button to open settings: +- Adjust interval (1-30 minutes) +- Start/Stop the timer +- Disabled by default on page load + +### Multi-Poke + +When enabled, an orange "10x" button appears next to each user's poke button. Clicking it sends 10 pokes in rapid succession with a countdown display. + +## Troubleshooting + +**Buttons not appearing?** +- Make sure Tampermonkey is enabled +- Refresh the page +- Check that the script is enabled in Tampermonkey dashboard + +**Keep Alive not working?** +- Make sure you have a public chat room open (General Chat, Directing Room, etc.) +- Check browser console for error messages + +**Buttons disappeared off-screen?** +- Click the small "R" button in the top-left corner to reset positions +- This can happen when switching between different sized monitors + +**Positions reset unexpectedly?** +- Positions are stored in localStorage. Clearing browser data will reset them. + +## Updating + +The script checks for updates automatically. You can also manually update: +1. Click the Tampermonkey icon +2. Go to Dashboard +3. Click the script name +4. Click "Check for updates" diff --git a/cams.user.js b/cams.user.js new file mode 100644 index 0000000..8252d4a --- /dev/null +++ b/cams.user.js @@ -0,0 +1,951 @@ +// ==UserScript== +// @name cams +// @namespace http://tampermonkey.net/ +// @version 1.2.7 +// @description Set maxDockedCamsForUser, keep-alive, and multi-poke +// @author You +// @match https://chat.fabswingers.com/* +// @updateURL https://git.upto.im/geekery/scripts/raw/branch/main/meta.js +// @downloadURL https://git.upto.im/geekery/scripts/raw/branch/main/cams.user.js +// @grant none +// @run-at document-idle +// ==/UserScript== + +(function() { + 'use strict'; + + const BUTTON_ID = 'maxDockedCamsButton_SINGLETON'; + const KEEPALIVE_BUTTON_ID = 'keepAliveButton_SINGLETON'; + const MULTIPOKE_BUTTON_ID = 'multiPokeButton_SINGLETON'; + const RESET_BUTTON_ID = 'resetPositionsButton_SINGLETON'; + const LOCK_KEY = 'maxDockedCams_scriptLock'; + const KEEPALIVE_SETTINGS_KEY = 'keepAlive_settings'; + const MULTIPOKE_SETTINGS_KEY = 'multiPoke_settings'; + const BUTTON_POSITIONS_KEY = 'buttonPositions'; + + // Default button positions (centered at top) + const DEFAULT_POSITIONS = { + [BUTTON_ID]: { left: 'calc(50% + 60px)', top: '10px' }, + [KEEPALIVE_BUTTON_ID]: { left: 'calc(50% - 70px)', top: '10px' }, + [MULTIPOKE_BUTTON_ID]: { left: 'calc(50% - 170px)', top: '10px' } + }; + + // Load saved button positions + function loadButtonPositions() { + try { + const saved = localStorage.getItem(BUTTON_POSITIONS_KEY); + if (saved) { + return JSON.parse(saved); + } + } catch (e) { + console.log('Error loading button positions', e); + } + return {}; + } + + function saveButtonPosition(buttonId, x, y) { + const positions = loadButtonPositions(); + positions[buttonId] = { x, y }; + localStorage.setItem(BUTTON_POSITIONS_KEY, JSON.stringify(positions)); + } + + function resetButtonPositions() { + localStorage.removeItem(BUTTON_POSITIONS_KEY); + + // Reset each button to default position + const maxDockedBtn = document.getElementById(BUTTON_ID); + const keepAliveBtn = document.getElementById(KEEPALIVE_BUTTON_ID); + const multiPokeBtn = document.getElementById(MULTIPOKE_BUTTON_ID); + + if (maxDockedBtn) { + maxDockedBtn.style.right = 'auto'; + maxDockedBtn.style.left = DEFAULT_POSITIONS[BUTTON_ID].left; + maxDockedBtn.style.top = DEFAULT_POSITIONS[BUTTON_ID].top; + } + if (keepAliveBtn) { + keepAliveBtn.style.right = 'auto'; + keepAliveBtn.style.left = DEFAULT_POSITIONS[KEEPALIVE_BUTTON_ID].left; + keepAliveBtn.style.top = DEFAULT_POSITIONS[KEEPALIVE_BUTTON_ID].top; + } + if (multiPokeBtn) { + multiPokeBtn.style.right = 'auto'; + multiPokeBtn.style.left = DEFAULT_POSITIONS[MULTIPOKE_BUTTON_ID].left; + multiPokeBtn.style.top = DEFAULT_POSITIONS[MULTIPOKE_BUTTON_ID].top; + } + + console.log('Button positions reset to defaults'); + } + + function makeDraggable(button) { + let isDragging = false; + let wasDragged = false; + let startX, startY, initialX, initialY; + + button.style.cursor = 'move'; + + button.addEventListener('mousedown', function(e) { + if (e.button !== 0) return; // Only left click + isDragging = true; + wasDragged = false; + startX = e.clientX; + startY = e.clientY; + initialX = button.offsetLeft; + initialY = button.offsetTop; + e.preventDefault(); + }); + + document.addEventListener('mousemove', function(e) { + if (!isDragging) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + // Only count as drag if moved more than 5px + if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { + wasDragged = true; + } + + let newX = initialX + deltaX; + let newY = initialY + deltaY; + + // Keep within viewport + newX = Math.max(0, Math.min(newX, window.innerWidth - button.offsetWidth)); + newY = Math.max(0, Math.min(newY, window.innerHeight - button.offsetHeight)); + + button.style.left = newX + 'px'; + button.style.top = newY + 'px'; + button.style.right = 'auto'; + }); + + document.addEventListener('mouseup', function(e) { + if (isDragging) { + isDragging = false; + if (wasDragged) { + saveButtonPosition(button.id, button.offsetLeft, button.offsetTop); + } + } + }); + + // Prevent click event if we were dragging + button.addEventListener('click', function(e) { + if (wasDragged) { + e.preventDefault(); + e.stopPropagation(); + wasDragged = false; + } + }, true); + + // Apply saved position if exists + const positions = loadButtonPositions(); + if (positions[button.id]) { + button.style.left = positions[button.id].x + 'px'; + button.style.top = positions[button.id].y + 'px'; + button.style.right = 'auto'; + } + } + + // Public room names to match against + const PUBLIC_ROOMS = [ + 'General Chat', + 'Directing Room', + 'Directing Room #2', + 'Directing Room #3', + 'Directing Room #4', + 'Directing Room #5', + 'Directing Room #6', + 'Site Supporters', + 'Verified Users', + 'Scotland Swingers', + 'Northern Swingers', + 'South East Swing', + 'South West Swing', + 'Midlands Swing', + 'East Anglia', + 'Ireland Swingers', + 'London Swingers', + 'Wales Swingers', + 'USA Swingers', + 'Canadian Swingers', + 'Bisexual Chat', + 'Bi Directing Room' + ]; + + const DEFAULT_INTERVAL = 10; // minutes + const DEFAULT_POKE_COUNT = 10; + const POKE_DELAY = 150; // ms between pokes + + let keepAliveInterval = null; + let isKeepAliveRunning = false; + let keepAliveSettings = loadKeepAliveSettings(); + + let multiPokeEnabled = loadMultiPokeSettings(); + + function loadMultiPokeSettings() { + return localStorage.getItem(MULTIPOKE_SETTINGS_KEY) !== 'false'; + } + + function saveMultiPokeSettings() { + localStorage.setItem(MULTIPOKE_SETTINGS_KEY, multiPokeEnabled.toString()); + } + + function loadKeepAliveSettings() { + try { + const saved = localStorage.getItem(KEEPALIVE_SETTINGS_KEY); + if (saved) { + return JSON.parse(saved); + } + } catch (e) { + console.log('KeepAlive: Error loading settings', e); + } + return { intervalMinutes: DEFAULT_INTERVAL }; + } + + function saveKeepAliveSettings() { + localStorage.setItem(KEEPALIVE_SETTINGS_KEY, JSON.stringify(keepAliveSettings)); + } + + // Try to acquire lock + const lockValue = Date.now().toString(); + + if (localStorage.getItem(LOCK_KEY)) { + console.log('MaxDockedCams: Another instance already running, exiting'); + return; + } + + localStorage.setItem(LOCK_KEY, lockValue); + + window.addEventListener('beforeunload', function() { + if (localStorage.getItem(LOCK_KEY) === lockValue) { + localStorage.removeItem(LOCK_KEY); + } + }); + + // Find a public room that the user has joined + function findPublicRoom() { + // Try multiple selectors for chat windows + const chatWindows = document.querySelectorAll('[id^="CHATWINDOW"], [id^="ChatWindow"], [id^="chatwindow"], .x-window'); + + for (const win of chatWindows) { + // Try multiple selectors for the title + const titleEl = win.querySelector('.x-window-header-text') || + win.querySelector('.x-window-header span') || + win.querySelector('[class*="header"] span'); + if (titleEl) { + const title = titleEl.textContent.trim(); + for (const publicRoom of PUBLIC_ROOMS) { + // Use startsWith to match "General Chat" in "General Chat (45)" + if (title.startsWith(publicRoom)) { + // Extract room ID from window ID (format: CHATWINDOW08967111-b7ae-496c-...) + const roomId = win.id.replace(/^CHATWINDOW/i, '').replace(/^ChatWindow_?/i, ''); + if (roomId) { + console.log(`KeepAlive: Found public room "${title}" with ID ${roomId}`); + return { roomId, title, windowId: win.id }; + } + } + } + } + } + + // Fallback: check RoomListStore + if (typeof RoomListStore !== 'undefined' && RoomListStore.data) { + const rooms = RoomListStore.data.items || []; + for (const room of rooms) { + const roomData = room.data || room; + const name = roomData.Name || ''; + for (const publicRoom of PUBLIC_ROOMS) { + if (name.startsWith(publicRoom)) { + console.log(`KeepAlive: Found public room "${name}" with ID ${roomData.ID}`); + return { roomId: roomData.ID, title: name }; + } + } + } + } + + // Debug: log what windows we can find + console.log('KeepAlive: Debug - searching for windows...'); + document.querySelectorAll('.x-window').forEach(w => { + console.log('KeepAlive: Found window:', w.id, w.querySelector('.x-window-header-text')?.textContent); + }); + + return null; + } + + function getUserId() { + if (typeof userID !== 'undefined') return userID; + if (typeof window.userID !== 'undefined') return window.userID; + if (typeof _currentUserID !== 'undefined') return _currentUserID; + if (typeof currentUserID !== 'undefined') return currentUserID; + return null; + } + + function getAuthCode() { + if (typeof auth_code !== 'undefined') return auth_code; + if (typeof window.auth_code !== 'undefined') return window.auth_code; + if (typeof _authCode !== 'undefined') return _authCode; + if (typeof authCode !== 'undefined') return authCode; + return null; + } + + function sendKeepAlive() { + const room = findPublicRoom(); + if (!room) { + console.log('KeepAlive: No public room found'); + return false; + } + + // Find the chat input field for this room + // Look for input field within the chat window + const chatWindow = room.windowId ? document.getElementById(room.windowId) : + document.getElementById('CHATWINDOW' + room.roomId) || + document.getElementById('ChatWindow_' + room.roomId) || + document.querySelector(`[id*="${room.roomId}"].x-window`); + + if (!chatWindow) { + console.log('KeepAlive: Could not find chat window element'); + return sendKeepAliveViaAPI(room); + } + + // Find the text input field - usually a textarea or input within the window + const inputField = chatWindow.querySelector('textarea') || + chatWindow.querySelector('input[type="text"]') || + chatWindow.querySelector('.x-form-text'); + + if (!inputField) { + console.log('KeepAlive: Could not find input field, trying API method'); + return sendKeepAliveViaAPI(room); + } + + // Store current value, set our message, trigger submit, restore + const originalValue = inputField.value; + inputField.value = ' '; + inputField.focus(); + + // Trigger input event + inputField.dispatchEvent(new Event('input', { bubbles: true })); + inputField.dispatchEvent(new Event('change', { bubbles: true })); + + // Simulate Enter key press to submit + const enterEvent = new KeyboardEvent('keydown', { + key: 'Enter', + code: 'Enter', + keyCode: 13, + which: 13, + bubbles: true + }); + inputField.dispatchEvent(enterEvent); + + const enterPress = new KeyboardEvent('keypress', { + key: 'Enter', + code: 'Enter', + keyCode: 13, + which: 13, + bubbles: true + }); + inputField.dispatchEvent(enterPress); + + const enterUp = new KeyboardEvent('keyup', { + key: 'Enter', + code: 'Enter', + keyCode: 13, + which: 13, + bubbles: true + }); + inputField.dispatchEvent(enterUp); + + console.log(`KeepAlive: Simulated input to "${room.title}" at ${new Date().toLocaleTimeString()}`); + return true; + } + + function sendKeepAliveViaAPI(room) { + const userId = getUserId(); + if (!userId) { + console.log('KeepAlive: User ID not found'); + return false; + } + + const authCodeVal = getAuthCode(); + + if (typeof Coolite !== 'undefined' && Coolite.AjaxMethods && Coolite.AjaxMethods.AddChatLine) { + Coolite.AjaxMethods.AddChatLine( + room.roomId, + ' ', + userId, + window._ipaddress, + window._urlwebchatid, + window._urlwebchatkey, + false, + false, + false, + '', + authCodeVal, + { + success: function() { + console.log(`KeepAlive: Sent via API to "${room.title}" at ${new Date().toLocaleTimeString()}`); + }, + failure: function(error) { + console.log('KeepAlive: Failed to send message', error); + } + } + ); + return true; + } else { + console.log('KeepAlive: Coolite.AjaxMethods.AddChatLine not available'); + return false; + } + } + + function startKeepAlive() { + if (isKeepAliveRunning) return; + + const intervalMs = keepAliveSettings.intervalMinutes * 60 * 1000; + keepAliveInterval = setInterval(sendKeepAlive, intervalMs); + isKeepAliveRunning = true; + updateKeepAliveButtonState(); + console.log(`KeepAlive: Started with ${keepAliveSettings.intervalMinutes} minute interval`); + + sendKeepAlive(); + } + + function stopKeepAlive() { + if (keepAliveInterval) { + clearInterval(keepAliveInterval); + keepAliveInterval = null; + } + isKeepAliveRunning = false; + updateKeepAliveButtonState(); + console.log('KeepAlive: Stopped'); + } + + function updateKeepAliveButtonState() { + const button = document.getElementById(KEEPALIVE_BUTTON_ID); + if (button) { + if (isKeepAliveRunning) { + button.style.backgroundColor = '#f44336'; + button.textContent = `Keep Alive ON (${keepAliveSettings.intervalMinutes}m)`; + } else { + button.style.backgroundColor = '#2196F3'; + button.textContent = 'Keep Alive OFF'; + } + } + } + + function showKeepAliveDialog() { + const overlay = document.createElement('div'); + overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9999999; display: flex; align-items: center; justify-content: center;'; + + const dialog = document.createElement('div'); + dialog.style.cssText = 'background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); min-width: 300px;'; + + dialog.innerHTML = ` +
+

Keep Alive Settings

+

Interval (minutes):

+ +

Status: ${isKeepAliveRunning ? 'Running' : 'Stopped'}

+
+ + + +
+
+ `; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + const input = document.getElementById('keepAliveIntervalInput'); + const saveBtn = document.getElementById('keepAliveSaveBtn'); + const cancelBtn = document.getElementById('keepAliveCancelBtn'); + const toggleBtn = document.getElementById('keepAliveToggleBtn'); + + setTimeout(() => input.focus(), 100); + + saveBtn.onclick = function() { + const value = parseInt(input.value, 10); + if (value >= 1 && value <= 30) { + keepAliveSettings.intervalMinutes = value; + saveKeepAliveSettings(); + + if (isKeepAliveRunning) { + stopKeepAlive(); + startKeepAlive(); + } + + updateKeepAliveButtonState(); + overlay.remove(); + } else { + alert('Please enter a number between 1 and 30'); + } + }; + + cancelBtn.onclick = function() { + overlay.remove(); + }; + + toggleBtn.onclick = function() { + if (isKeepAliveRunning) { + stopKeepAlive(); + } else { + const value = parseInt(input.value, 10); + if (value >= 1 && value <= 30) { + keepAliveSettings.intervalMinutes = value; + saveKeepAliveSettings(); + } + startKeepAlive(); + } + overlay.remove(); + }; + + input.onkeypress = function(e) { + if (e.key === 'Enter') { + saveBtn.click(); + } + }; + + overlay.onkeydown = function(e) { + if (e.key === 'Escape') { + cancelBtn.click(); + } + }; + } + + // ==================== MULTI-POKE FUNCTIONALITY ==================== + + function performMultiPoke(roomId, userId, buttonElement) { + const originalText = buttonElement.textContent; + let pokeCount = 0; + const totalPokes = DEFAULT_POKE_COUNT; + + // Disable the button during multi-poke + buttonElement.style.opacity = '0.5'; + buttonElement.style.pointerEvents = 'none'; + + function sendPoke() { + if (pokeCount < totalPokes) { + try { + if (typeof window.PK === 'function') { + window.PK(roomId, userId); + } else if (typeof Coolite !== 'undefined' && Coolite.AjaxMethods && Coolite.AjaxMethods.Poke) { + Coolite.AjaxMethods.Poke(roomId, '', userId, '', ''); + } else { + const tempPokeLink = document.createElement('a'); + tempPokeLink.href = `javascript:PK('${roomId}','${userId}')`; + tempPokeLink.style.display = 'none'; + document.body.appendChild(tempPokeLink); + tempPokeLink.click(); + document.body.removeChild(tempPokeLink); + } + } catch (error) { + console.log('Poke error:', error); + } + + pokeCount++; + buttonElement.textContent = `${totalPokes - pokeCount}x`; + + if (pokeCount < totalPokes) { + setTimeout(sendPoke, POKE_DELAY); + } else { + setTimeout(() => { + buttonElement.textContent = originalText; + buttonElement.style.opacity = ''; + buttonElement.style.pointerEvents = ''; + }, 500); + } + } + } + + sendPoke(); + } + + function addMissingPokeButtons() { + if (!multiPokeEnabled) return; + + // Find all user rows + const userRows = document.querySelectorAll('.ur'); + + userRows.forEach(userRow => { + const allDiv = userRow.querySelector('.all'); + if (!allDiv) return; + if (allDiv.innerHTML.trim() === '') return; + + // Check if this user already has a poke button + if (allDiv.querySelector('a[href*="PK("]')) return; + + // Try to get room ID and user ID from block button + const blockButton = allDiv.querySelector('a[href*="BU("]'); + if (!blockButton) return; + + const blockHref = blockButton.getAttribute('href'); + const userIdMatch = blockHref.match(/BU\('[^']+','([^']+)'\)/); + const roomIdMatch = blockHref.match(/BU\('([^']+)'/); + + if (!userIdMatch || !roomIdMatch) return; + + const odlUserId = userIdMatch[1]; + const roomId = roomIdMatch[1]; + + // Find insertion point (before block button) + const insertionPoint = blockButton; + + // Add Poke button + const pokeLink = document.createElement('a'); + pokeLink.className = 'ula'; + pokeLink.href = `javascript:PK('${roomId}','${odlUserId}')`; + pokeLink.textContent = 'poke'; + pokeLink.setAttribute('data-added-by-script', 'true'); + + allDiv.insertBefore(pokeLink, insertionPoint); + const pokeSpace = document.createTextNode('\u00A0'); + allDiv.insertBefore(pokeSpace, insertionPoint); + + console.log(`Added poke button for user ${odlUserId}`); + }); + } + + function addMultiPokeButtons() { + if (!multiPokeEnabled) return; + + // First add missing poke buttons + addMissingPokeButtons(); + + console.log('Adding/updating multi-poke buttons...'); + + // Find ALL poke buttons on the page + const pokeButtons = document.querySelectorAll('a[href*="PK("]'); + + pokeButtons.forEach(pokeButton => { + // Check if multi-poke button already exists next to this poke button + const parent = pokeButton.parentNode; + if (!parent) return; + + // Skip if already has a multi-poke button nearby + let nextSib = pokeButton.nextSibling; + while (nextSib) { + if (nextSib.nodeType === Node.ELEMENT_NODE && nextSib.classList && nextSib.classList.contains('multi-poke-btn')) { + return; // Already has one + } + // Only check immediate siblings (skip text nodes) + if (nextSib.nodeType === Node.ELEMENT_NODE) break; + nextSib = nextSib.nextSibling; + } + + const pokeHref = pokeButton.getAttribute('href'); + const match = pokeHref.match(/PK\('([^']+)','([^']+)'\)/); + if (!match) return; + + const roomId = match[1]; + const odlUserId = match[2]; + + const multiPokeBtn = document.createElement('a'); + multiPokeBtn.className = 'multi-poke-btn'; + multiPokeBtn.href = 'javascript:void(0)'; + multiPokeBtn.textContent = '10x'; + multiPokeBtn.title = 'Send 10 pokes'; + multiPokeBtn.setAttribute('data-multi-poke', 'true'); + multiPokeBtn.setAttribute('data-user-id', odlUserId); + + multiPokeBtn.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + performMultiPoke(roomId, odlUserId, this); + }); + + const space = document.createTextNode('\u00A0'); + pokeButton.parentNode.insertBefore(space, pokeButton.nextSibling); + pokeButton.parentNode.insertBefore(multiPokeBtn, space.nextSibling); + + console.log(`Added multi-poke button for user ${odlUserId}`); + }); + } + + function removeMultiPokeButtons() { + const multiPokeElements = document.querySelectorAll('.multi-poke-btn, [data-multi-poke="true"]'); + multiPokeElements.forEach(element => { + if (element.previousSibling && element.previousSibling.nodeType === Node.TEXT_NODE) { + element.previousSibling.remove(); + } + element.remove(); + }); + } + + function toggleMultiPoke() { + multiPokeEnabled = !multiPokeEnabled; + saveMultiPokeSettings(); + updateMultiPokeButtonState(); + + if (multiPokeEnabled) { + addMultiPokeButtons(); + } else { + removeMultiPokeButtons(); + } + + console.log('Multi-poke', multiPokeEnabled ? 'enabled' : 'disabled'); + } + + function updateMultiPokeButtonState() { + const button = document.getElementById(MULTIPOKE_BUTTON_ID); + if (button) { + if (multiPokeEnabled) { + button.style.backgroundColor = '#ff6b35'; + button.textContent = '10x ON'; + } else { + button.style.backgroundColor = '#6c757d'; + button.textContent = '10x OFF'; + } + } + } + + function observeUserList() { + const targetNode = document.querySelector('div[id*="CHATUSERLABEL"]') || + document.querySelector('.x-panel-body') || + document.body; + + const observer = new MutationObserver(function(mutations) { + let shouldUpdate = false; + mutations.forEach(function(mutation) { + if (mutation.type === 'childList') { + mutation.addedNodes.forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE && + (node.classList && node.classList.contains('ur') || node.querySelector && node.querySelector('.ur'))) { + shouldUpdate = true; + } + }); + } + if (mutation.type === 'childList' && mutation.target.classList && mutation.target.classList.contains('ur')) { + shouldUpdate = true; + } + }); + + if (shouldUpdate && multiPokeEnabled) { + clearTimeout(window.multiPokeUpdateDebounce); + window.multiPokeUpdateDebounce = setTimeout(() => { + addMultiPokeButtons(); + }, 200); + } + }); + + observer.observe(targetNode, { + childList: true, + subtree: true + }); + } + + // ==================== END MULTI-POKE ==================== + + // Create custom dialog for MaxDockedCams + function showCustomPrompt() { + const overlay = document.createElement('div'); + overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9999999; display: flex; align-items: center; justify-content: center;'; + + const dialog = document.createElement('div'); + dialog.style.cssText = 'background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); min-width: 300px;'; + + dialog.innerHTML = ` +
+

Set MaxDockedCams

+

Enter a number:

+ +
+ + +
+
+ `; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + const input = document.getElementById('maxDockedInput'); + const okBtn = document.getElementById('okBtn'); + const cancelBtn = document.getElementById('cancelBtn'); + + setTimeout(() => input.focus(), 100); + + okBtn.onclick = function() { + const value = input.value; + if (value === '') { + alert('Please enter a number'); + return; + } + + const number = parseInt(value, 10); + if (!isNaN(number)) { + window.maxDockedCamsForUser = number; + console.log(`maxDockedCamsForUser set to: ${number}`); + overlay.remove(); + alert(`maxDockedCamsForUser successfully set to ${number}`); + } else { + alert('Please enter a valid number'); + } + }; + + cancelBtn.onclick = function() { + console.log('User cancelled'); + overlay.remove(); + }; + + input.onkeypress = function(e) { + if (e.key === 'Enter') { + okBtn.click(); + } + }; + + overlay.onkeydown = function(e) { + if (e.key === 'Escape') { + cancelBtn.click(); + } + }; + } + + // Wait and create buttons + setTimeout(function() { + // Cleanup existing buttons + document.querySelectorAll(`[id^="maxDockedCamsButton"]`).forEach(btn => btn.remove()); + document.querySelectorAll(`[id^="keepAliveButton"]`).forEach(btn => btn.remove()); + + // MaxDockedCams button + const maxDockedButton = document.createElement('button'); + maxDockedButton.id = BUTTON_ID; + maxDockedButton.textContent = 'Set MaxDockedCams'; + maxDockedButton.type = 'button'; + maxDockedButton.style.cssText = 'position: fixed; top: 10px; left: calc(50% + 60px); z-index: 999999; padding: 8px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3);'; + + maxDockedButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + showCustomPrompt(); + }); + + document.body.appendChild(maxDockedButton); + makeDraggable(maxDockedButton); + console.log('MaxDockedCams button created'); + + // Keep Alive button + const keepAliveButton = document.createElement('button'); + keepAliveButton.id = KEEPALIVE_BUTTON_ID; + keepAliveButton.type = 'button'; + keepAliveButton.style.cssText = 'position: fixed; top: 10px; left: calc(50% - 70px); z-index: 999999; padding: 8px 12px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3);'; + + keepAliveButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + showKeepAliveDialog(); + }); + + document.body.appendChild(keepAliveButton); + makeDraggable(keepAliveButton); + updateKeepAliveButtonState(); + console.log('KeepAlive button created'); + + // Multi-Poke toggle button + document.querySelectorAll(`[id^="multiPokeButton"]`).forEach(btn => btn.remove()); + const multiPokeButton = document.createElement('button'); + multiPokeButton.id = MULTIPOKE_BUTTON_ID; + multiPokeButton.type = 'button'; + multiPokeButton.style.cssText = 'position: fixed; top: 10px; left: calc(50% - 170px); z-index: 999999; padding: 8px 12px; background-color: #ff6b35; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3);'; + + multiPokeButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + toggleMultiPoke(); + }); + + document.body.appendChild(multiPokeButton); + makeDraggable(multiPokeButton); + updateMultiPokeButtonState(); + console.log('MultiPoke button created'); + + // Reset positions button (small, top-left) + document.querySelectorAll(`#${RESET_BUTTON_ID}`).forEach(btn => btn.remove()); + const resetButton = document.createElement('button'); + resetButton.id = RESET_BUTTON_ID; + resetButton.textContent = 'R'; + resetButton.title = 'Reset button positions'; + resetButton.type = 'button'; + resetButton.style.cssText = 'position: fixed; top: 5px; left: 5px; z-index: 999999; padding: 2px 6px; background-color: #888; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 10px; font-weight: bold; opacity: 0.5;'; + + resetButton.addEventListener('mouseenter', function() { + this.style.opacity = '1'; + }); + resetButton.addEventListener('mouseleave', function() { + this.style.opacity = '0.5'; + }); + resetButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + resetButtonPositions(); + }); + + document.body.appendChild(resetButton); + console.log('Reset button created'); + + // Initialize multi-poke if enabled + setTimeout(function() { + if (multiPokeEnabled) { + addMultiPokeButtons(); + } + observeUserList(); + + // Periodic check for new users + setInterval(() => { + if (multiPokeEnabled) { + addMultiPokeButtons(); + } + }, 2000); + }, 1000); + + // Cleanup duplicate buttons periodically + setInterval(function() { + const maxDockedButtons = document.querySelectorAll(`#${BUTTON_ID}`); + if (maxDockedButtons.length > 1) { + for (let i = 1; i < maxDockedButtons.length; i++) { + maxDockedButtons[i].remove(); + } + } + + const keepAliveButtons = document.querySelectorAll(`#${KEEPALIVE_BUTTON_ID}`); + if (keepAliveButtons.length > 1) { + for (let i = 1; i < keepAliveButtons.length; i++) { + keepAliveButtons[i].remove(); + } + } + + const multiPokeButtons = document.querySelectorAll(`#${MULTIPOKE_BUTTON_ID}`); + if (multiPokeButtons.length > 1) { + for (let i = 1; i < multiPokeButtons.length; i++) { + multiPokeButtons[i].remove(); + } + } + }, 500); + + }, 100); + + // Add CSS for multi-poke buttons and added poke buttons + const style = document.createElement('style'); + style.textContent = ` + .multi-poke-btn { + background-color: #ff6b35 !important; + color: white !important; + padding: 1px 4px !important; + border-radius: 3px !important; + text-decoration: none !important; + font-weight: bold !important; + font-size: 10px !important; + margin-left: 2px !important; + } + .multi-poke-btn:hover { + background-color: #ff8c42 !important; + color: white !important; + } + .ula { + color: #666 !important; + text-decoration: underline !important; + cursor: pointer !important; + } + .ula:hover { + color: #007acc !important; + } + `; + document.head.appendChild(style); + +})(); diff --git a/meta.js b/meta.js new file mode 100644 index 0000000..567fd79 --- /dev/null +++ b/meta.js @@ -0,0 +1,12 @@ +// ==UserScript== +// @name cams +// @namespace http://tampermonkey.net/ +// @version 1.2.7 +// @description Set maxDockedCamsForUser, keep-alive, and multi-poke +// @author You +// @match https://chat.fabswingers.com/* +// @updateURL https://git.upto.im/geekery/scripts/raw/branch/main/meta.js +// @downloadURL https://git.upto.im/geekery/scripts/raw/branch/main/cams.user.js +// @grant none +// @run-at document-idle +// ==/UserScript== diff --git a/screen.png b/screen.png new file mode 100644 index 0000000..137100b Binary files /dev/null and b/screen.png differ