first push
This commit is contained in:
111
README.md
Normal file
111
README.md
Normal 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
951
cams.user.js
Normal 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
12
meta.js
Normal 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
BIN
screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Reference in New Issue
Block a user