first push

This commit is contained in:
Chatter
2026-01-29 09:22:24 -05:00
commit b26d0aecee
4 changed files with 1074 additions and 0 deletions

111
README.md Normal file
View File

@@ -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 <a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/" target="_blank">Tampermonkey for Firefox</a>
2. Click "Add to Firefox"
3. Click "Add" when prompted for permissions
#### Chrome
1. Go to <a href="https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo" target="_blank">Tampermonkey for Chrome</a>
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: **<a href="https://git.upto.im/geekery/scripts/raw/branch/main/cams.user.js" target="_blank">Install Script</a>**
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"

951
cams.user.js Normal file
View File

@@ -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 = `
<div style="font-family: Arial, sans-serif;">
<h3 style="margin: 0 0 15px 0; color: #333;">Keep Alive Settings</h3>
<p style="margin: 0 0 10px 0; color: #666;">Interval (minutes):</p>
<input type="number" id="keepAliveIntervalInput" value="${keepAliveSettings.intervalMinutes}" min="1" max="30" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; box-sizing: border-box;" />
<p style="margin: 10px 0; color: #888; font-size: 12px;">Status: ${isKeepAliveRunning ? 'Running' : 'Stopped'}</p>
<div style="margin-top: 15px; display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap;">
<button id="keepAliveCancelBtn" style="padding: 8px 15px; background: #ccc; color: #333; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">Cancel</button>
<button id="keepAliveToggleBtn" style="padding: 8px 15px; background: ${isKeepAliveRunning ? '#f44336' : '#4CAF50'}; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">${isKeepAliveRunning ? 'Stop' : 'Start'}</button>
<button id="keepAliveSaveBtn" style="padding: 8px 15px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">Save</button>
</div>
</div>
`;
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 = `
<div style="font-family: Arial, sans-serif;">
<h3 style="margin: 0 0 15px 0; color: #333;">Set MaxDockedCams</h3>
<p style="margin: 0 0 10px 0; color: #666;">Enter a number:</p>
<input type="number" id="maxDockedInput" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; box-sizing: border-box;" placeholder="Enter number" />
<div style="margin-top: 15px; text-align: right;">
<button id="cancelBtn" style="padding: 8px 15px; margin-right: 10px; background: #ccc; color: #333; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">Cancel</button>
<button id="okBtn" style="padding: 8px 15px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">OK</button>
</div>
</div>
`;
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);
})();

12
meta.js Normal file
View File

@@ -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==

BIN
screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB