Torn Floating Text Panel

A movable panel to manage text Copy Paste Format with fill feature.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==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();
    }

})();