// ==UserScript==
// @name Torn Floating Text Panel
// @namespace http://tampermonkey.net/
// @version 1.5
// @license MIT
// @description A movable panel to manage text Copy Paste Format with fill feature.
// @author NootNoot4 [3754506]
// @match *://www.torn.com/*
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION --- //
const defaultMessages = [
{
label: 'Open Bazaar With Xan',
text: `[S]BAZAAR OPEN!!
sell cheap 🧸🌺💿🔋 in my bazaar under MV ⬇️.
💊xan 813k💊
Check it in here
https://t.ly/Xn9mV`
},
{
label: 'Buy Items',
text: `[B] 💿dvd🔋cans
flw🌸plsh🧸alc🍺etc 96% MV
xan 800k
Mug free!
Price
https://t.ly/4cdiZ
Trade
https://t.ly/PA6v6`
}
];
const TORN_ICON_SVG = `<svg viewbox="0 0 24 24" width="20" height="20" fill="white" style="display: block;"><path d="M3 3h18v4H3z M10 8h4v13h-4z"></path></svg>`;
// --- UPDATED: More reliable selector for the chat's send button ---
const CHAT_SEND_BUTTON_SELECTOR = 'button[class*="iconWrapper___tyRRU"]';
// --- SCRIPT STATE --- //
let messages = [];
let mainContainer, floatingButton;
let isMinimized = false;
let isDragging = false;
let rfcvToken = null;
// Intercept network requests to capture the live rfcv token
const originalFetch = window.fetch;
window.fetch = function(...args) {
const url = args[0] instanceof Request ? args[0].url : args[0];
if (typeof url === 'string' && url.includes('rfcv=')) {
const match = url.match(/rfcv=([a-zA-Z0-9]+)/);
if (match && match[1]) { rfcvToken = match[1]; }
}
return originalFetch.apply(this, args);
};
const originalXhrOpen = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function(...args) {
const url = args[1];
if (typeof url === 'string' && url.includes('rfcv=')) {
const match = url.match(/rfcv=([a-zA-Z0-9]+)/);
if (match && match[1]) { rfcvToken = match[1]; }
}
return originalXhrOpen.apply(this, args);
};
// --- HELPER FUNCTIONS --- //
function showFlashMessage(text, isError = false, duration = 2500) {
const flashDiv = document.createElement('div');
flashDiv.textContent = text;
Object.assign(flashDiv.style, {
position: 'fixed', top: '20px', left: '50%', transform: 'translateX(-50%)',
padding: '12px 20px', borderRadius: '8px',
backgroundColor: isError ? '#f44336' : '#4CAF50', color: 'white',
zIndex: '99999999', fontSize: '16px', fontWeight: 'bold',
boxShadow: '0 4px 12px rgba(0,0,0,0.2)', opacity: '0',
transition: 'opacity 0.4s ease-in-out'
});
document.body.appendChild(flashDiv);
setTimeout(() => flashDiv.style.opacity = '1', 10);
setTimeout(() => {
flashDiv.style.opacity = '0';
setTimeout(() => flashDiv.remove(), 400);
}, duration);
}
function simulateUserInput(textarea, text) {
const nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newText = textarea.value.substring(0, start) + text + textarea.value.substring(end);
nativeTextareaValueSetter.call(textarea, newText);
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
textarea.selectionStart = textarea.selectionEnd = start + text.length;
textarea.focus();
}
function openModal(message = null, index = null) {
const isEditing = message !== null;
const backdrop = document.createElement('div');
Object.assign(backdrop.style, {
position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
backgroundColor: 'rgba(0,0,0,0.6)', zIndex: '9999990'
});
const modal = document.createElement('div');
Object.assign(modal.style, {
position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
width: '90%', maxWidth: '500px', background: '#282c34', color: 'white',
borderRadius: '8px', padding: '20px', boxShadow: '0 5px 15px rgba(0,0,0,0.3)',
display: 'flex', flexDirection: 'column', gap: '10px', zIndex: '9999991'
});
const labelInput = document.createElement('input');
if (!isEditing) {
labelInput.placeholder = 'Enter Label for new button...';
Object.assign(labelInput.style, { padding: '10px', fontSize: '14px', border: '1px solid #555', borderRadius: '5px', background: '#333', color: 'white' });
modal.appendChild(labelInput);
}
const textArea = document.createElement('textarea');
textArea.value = isEditing ? message.text : '';
textArea.placeholder = 'Enter text to copy...';
textArea.maxLength = 125;
Object.assign(textArea.style, { width: '100%', height: '200px', boxSizing: 'border-box', fontSize: '14px', fontFamily: 'monospace', padding: '10px', background: '#333', color: 'white', border: '1px solid #555' });
const charCounter = document.createElement('div');
Object.assign(charCounter.style, { textAlign: 'right', fontSize: '12px', color: '#aaa', fontFamily: 'monospace', marginTop: '-5px' });
const updateCounter = () => {
const currentLength = textArea.value.length;
charCounter.textContent = `${currentLength} / 125`;
charCounter.style.color = currentLength >= 125 ? '#f44336' : '#aaa';
};
textArea.addEventListener('input', updateCounter);
const buttonDiv = document.createElement('div');
Object.assign(buttonDiv.style, { display: 'flex', justifyContent: 'flex-end', gap: '10px' });
const saveButton = document.createElement('button');
saveButton.textContent = 'Save';
Object.assign(saveButton.style, { padding: '10px 20px', background: '#4CAF50', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' });
saveButton.onclick = async () => {
if (isEditing) messages[index].text = textArea.value;
else {
const newLabel = labelInput.value.trim();
if (!newLabel) { alert('Label cannot be empty.'); return; }
messages.push({ label: newLabel, text: textArea.value });
}
await GM_setValue('savedMessages', JSON.stringify(messages));
showFlashMessage('Saved successfully!');
backdrop.remove();
renderUI();
};
const cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
Object.assign(cancelButton.style, { padding: '10px 20px', background: '#888', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' });
cancelButton.onclick = () => backdrop.remove();
buttonDiv.append(cancelButton, saveButton);
modal.append(textArea, charCounter, buttonDiv);
backdrop.append(modal);
document.body.append(backdrop);
updateCounter();
(isEditing ? textArea : labelInput).focus();
}
function makeDraggable(element, handle, storageKey) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
handle.addEventListener('mousedown', dragStart);
handle.addEventListener('touchstart', dragStart, { passive: false });
function dragStart(e) {
isDragging = false;
if (e.type === 'touchstart') {
pos3 = e.touches[0].clientX;
pos4 = e.touches[0].clientY;
} else {
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
}
document.addEventListener('mouseup', dragEnd);
document.addEventListener('touchend', dragEnd);
document.addEventListener('mousemove', elementDrag);
document.addEventListener('touchmove', elementDrag, { passive: false });
}
function elementDrag(e) {
e.preventDefault();
isDragging = true;
let clientX, clientY;
if (e.type.includes('touch')) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
pos1 = pos3 - clientX;
pos2 = pos4 - clientY;
pos3 = clientX;
pos4 = clientY;
let newTop = element.offsetTop - pos2;
let newLeft = element.offsetLeft - pos1;
const screenW = window.innerWidth;
const screenH = window.innerHeight;
const elemW = element.offsetWidth;
const elemH = element.offsetHeight;
if (newLeft < 0) newLeft = 0;
if (newTop < 0) newTop = 0;
if (newLeft + elemW > screenW) newLeft = screenW - elemW;
if (newTop + elemH > screenH) newTop = screenH - elemH;
element.style.top = newTop + "px";
element.style.left = newLeft + "px";
}
async function dragEnd() {
document.removeEventListener('mouseup', dragEnd);
document.removeEventListener('touchend', dragEnd);
document.removeEventListener('mousemove', elementDrag);
document.removeEventListener('touchmove', elementDrag);
if (isDragging) {
await GM_setValue(storageKey, JSON.stringify({ top: element.style.top, left: element.style.left }));
}
setTimeout(() => { isDragging = false; }, 0);
}
}
// --- UI & VISIBILITY --- //
function renderUI() {
if (!mainContainer) return;
mainContainer.innerHTML = '';
const panelHeader = document.createElement('div');
Object.assign(panelHeader.style, {
padding: '10px 15px', backgroundColor: '#333', color: 'white', display: 'flex',
justifyContent: 'space-between', alignItems: 'center', cursor: 'move',
borderTopLeftRadius: '8px', borderTopRightRadius: '8px', flexShrink: '0'
});
const title = document.createElement('span');
title.textContent = 'Quick Text Panel';
title.style.fontWeight = 'bold';
const minimizeButton = document.createElement('button');
minimizeButton.innerHTML = TORN_ICON_SVG;
Object.assign(minimizeButton.style, {
background: '#555', color: 'white', border: 'none', borderRadius: '50%',
cursor: 'pointer', width: '28px', height: '28px', display: 'flex',
alignItems: 'center', justifyContent: 'center', transform: 'rotate(180deg)',
transition: 'transform 0.3s ease-in-out'
});
minimizeButton.addEventListener('click', () => {
if (isDragging) return;
setTimeout(async () => {
const currentPosition = { top: mainContainer.style.top, left: mainContainer.style.left };
floatingButton.style.top = currentPosition.top;
floatingButton.style.left = currentPosition.left;
await GM_setValue('iconPosition', JSON.stringify(currentPosition));
toggleMinimize(true);
}, 0);
});
panelHeader.append(title, minimizeButton);
mainContainer.appendChild(panelHeader);
const contentWrapper = document.createElement('div');
Object.assign(contentWrapper.style, { padding: '15px', display: 'flex', flexDirection: 'column', gap: '15px', overflowY: 'auto', flexGrow: '1' });
messages.forEach((message, index) => {
const card = document.createElement('div');
Object.assign(card.style, { background: '#333', border: '1px solid #555', borderRadius: '8px', padding: '15px', display: 'flex', flexDirection: 'column', gap: '10px' });
const cardHeader = document.createElement('div');
Object.assign(cardHeader.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #555', paddingBottom: '10px' });
const cardLabel = document.createElement('span');
cardLabel.textContent = message.label;
Object.assign(cardLabel.style, { fontWeight: 'bold', fontSize: '16px' });
const headerActions = document.createElement('div');
Object.assign(headerActions.style, { display: 'flex', gap: '8px' });
const editButton = document.createElement('button');
editButton.textContent = '✏️';
Object.assign(editButton.style, {
background: '#555', color: 'white', border: 'none',
borderRadius: '5px', cursor: 'pointer', width: '28px', height: '28px',
fontSize: '16px', display: 'flex', alignItems: 'center', justifyContent: 'center'
});
editButton.onclick = () => openModal(message, index);
const deleteButton = document.createElement('button');
deleteButton.innerHTML = '🗑️';
Object.assign(deleteButton.style, {
background: '#dc3545', color: 'white', border: 'none',
borderRadius: '5px', cursor: 'pointer', width: '28px', height: '28px',
fontSize: '16px', display: 'flex', alignItems: 'center', justifyContent: 'center'
});
deleteButton.onclick = async () => {
if (confirm(`Are you sure you want to delete the message: "${message.label}"?`)) {
messages.splice(index, 1);
await GM_setValue('savedMessages', JSON.stringify(messages));
showFlashMessage('Message deleted.');
renderUI();
}
};
headerActions.append(editButton, deleteButton);
cardHeader.append(cardLabel, headerActions);
const toolbar = document.createElement('div');
Object.assign(toolbar.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center' });
const leftButtons = document.createElement('div');
Object.assign(leftButtons.style, { display: 'flex', gap: '8px' });
const copyButton = document.createElement('button');
copyButton.textContent = 'Copy';
Object.assign(copyButton.style, { padding: '5px 10px', background: '#007bff', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '12px' });
copyButton.onclick = () => navigator.clipboard.writeText(message.text).then(() => showFlashMessage(`Copied "${message.label}"!`)).catch(err => showFlashMessage('Failed to copy.', true));
const fillButton = document.createElement('button');
fillButton.textContent = 'Fill';
Object.assign(fillButton.style, { padding: '5px 10px', background: '#6f42c1', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '12px' });
fillButton.onclick = () => {
const chatInput = document.querySelector('#chatRoot textarea');
if (chatInput) {
simulateUserInput(chatInput, message.text);
showFlashMessage(`Filled chat with "${message.label}"!`);
} else {
showFlashMessage('Torn chat input not found.', true);
}
};
const sendButton = document.createElement('button');
sendButton.textContent = 'Send';
Object.assign(sendButton.style, { padding: '5px 10px', background: '#28a745', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '12px' });
sendButton.onclick = () => {
const tornSendButton = document.querySelector(CHAT_SEND_BUTTON_SELECTOR);
if (tornSendButton && !tornSendButton.disabled) {
tornSendButton.click();
showFlashMessage('Message sent!');
} else {
showFlashMessage('Send button not found or is disabled.', true);
}
};
leftButtons.append(copyButton, fillButton, sendButton);
toolbar.append(leftButtons);
const textPreview = document.createElement('pre');
textPreview.textContent = message.text;
Object.assign(textPreview.style, {
margin: '0', padding: '10px', background: '#222', color: 'white',
borderRadius: '5px', fontSize: '12px', whiteSpace: 'pre-wrap', wordBreak: 'break-word'
});
card.append(cardHeader, toolbar, textPreview);
contentWrapper.appendChild(card);
});
const bottomToolbar = document.createElement('div');
Object.assign(bottomToolbar.style, { marginTop: '10px', display: 'flex', gap: '10px' });
const addButton = document.createElement('button');
addButton.textContent = 'Add New Message';
Object.assign(addButton.style, { padding: '8px 10px', background: '#28a745', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', flexGrow: '1' });
addButton.onclick = () => openModal();
const resetButton = document.createElement('button');
resetButton.textContent = 'Reset All';
Object.assign(resetButton.style, { padding: '8px 10px', background: '#6c757d', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' });
resetButton.onclick = async () => {
if (confirm('Are you sure you want to reset everything? This will reset all text and panel positions.')) {
await GM_setValue('savedMessages', JSON.stringify(defaultMessages));
await GM_setValue('containerPosition', null);
await GM_setValue('iconPosition', null);
await GM_setValue('isMinimized', false);
showFlashMessage('Reset complete. Reloading...');
setTimeout(() => location.reload(), 1500);
}
};
bottomToolbar.append(addButton, resetButton);
contentWrapper.appendChild(bottomToolbar);
mainContainer.appendChild(contentWrapper);
makeDraggable(mainContainer, panelHeader, 'containerPosition');
}
async function toggleMinimize(minimize) {
isMinimized = minimize;
mainContainer.style.display = isMinimized ? 'none' : 'flex';
floatingButton.style.display = isMinimized ? 'flex' : 'none';
await GM_setValue('isMinimized', isMinimized);
}
async function initialize() {
try {
const initialRfcvMatch = document.documentElement.innerHTML.match(/var rfcv = "(\w+)"/);
if (initialRfcvMatch && initialRfcvMatch[1]) { rfcvToken = initialRfcvMatch[1]; }
} catch(e) { /* Fail silently */ }
messages = JSON.parse(await GM_getValue('savedMessages', null)) || defaultMessages;
isMinimized = await GM_getValue('isMinimized', false);
mainContainer = document.createElement('div');
Object.assign(mainContainer.style, {
position: 'fixed', zIndex: '999990', borderRadius: '8px', boxShadow: '0 2px 10px rgba(0,0,0,0.5)',
display: 'none', flexDirection: 'column', maxHeight: '85vh',
background: 'rgba(15, 15, 15, 0.95)', width: '250px'
});
const savedPosition = JSON.parse(await GM_getValue('containerPosition', null));
if (savedPosition) { mainContainer.style.top = savedPosition.top; mainContainer.style.left = savedPosition.left; }
else { mainContainer.style.top = '80px'; mainContainer.style.left = '20px'; }
floatingButton = document.createElement('button');
floatingButton.innerHTML = TORN_ICON_SVG;
Object.assign(floatingButton.style, {
position: 'fixed', zIndex: '999990', background: '#333', border: '2px solid #555',
color: 'white', borderRadius: '50%', cursor: 'pointer', width: '44px', height: '44px',
display: 'none', alignItems: 'center', justifyContent: 'center', boxShadow: '0 2px 8px rgba(0,0,0,0.3)'
});
const iconSavedPosition = JSON.parse(await GM_getValue('iconPosition', null));
if (iconSavedPosition) { floatingButton.style.top = iconSavedPosition.top; floatingButton.style.left = iconSavedPosition.left; }
else { floatingButton.style.bottom = '20px'; floatingButton.style.left = '20px'; }
floatingButton.addEventListener('click', () => {
if (isDragging) return;
setTimeout(async () => {
const currentPosition = { top: floatingButton.style.top, left: floatingButton.style.left };
mainContainer.style.top = currentPosition.top;
mainContainer.style.left = currentPosition.left;
await GM_setValue('containerPosition', JSON.stringify(currentPosition));
toggleMinimize(false);
}, 0);
});
makeDraggable(floatingButton, floatingButton, 'iconPosition');
document.body.append(mainContainer, floatingButton);
renderUI();
toggleMinimize(isMinimized);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();