Torn Quick-Text Panel (Char Limit)

A panel to manage text snippets with a 125-character limit, persistent storage, and full CRUD functionality.

// ==UserScript==
// @name         Torn Quick-Text Panel (Char Limit)
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  A panel to manage text snippets with a 125-character limit, persistent storage, and full CRUD functionality.
// @author       NootNoot4 [3754506]
// @match        *://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/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>`;

    // --- SCRIPT STATE --- //
    let messages = [];
    let mainContainer, floatingButton;
    let isMinimized = false;
    let isDragging = false;

    // --- HELPER FUNCTIONS --- //

    function showFlashMessage(text, isError = false) {
        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', // Highest z-index
            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);
        }, 2500);
    }

    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' // High z-index
        });
        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' // Higher than backdrop
        });
        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: true });

        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', dragMove);
            document.addEventListener('touchmove', dragMove, { passive: false });
        }

        function dragMove(e) {
            isDragging = true;
            let clientX, clientY;
            if (e.type === 'touchmove') {
                e.preventDefault();
                clientX = e.touches[0].clientX;
                clientY = e.touches[0].clientY;
            } else {
                e.preventDefault();
                clientX = e.clientX;
                clientY = e.clientY;
            }
            pos1 = pos3 - clientX;
            pos2 = pos4 - clientY;
            pos3 = clientX;
            pos4 = clientY;
            element.style.top = (element.offsetTop - pos2) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
        }

        async function dragEnd() {
            document.removeEventListener('mouseup', dragEnd);
            document.removeEventListener('touchend', dragEnd);
            document.removeEventListener('mousemove', dragMove);
            document.removeEventListener('touchmove', dragMove);
            if (isDragging) {
                await GM_setValue(storageKey, JSON.stringify({ top: element.style.top, left: element.style.left }));
            }
        }
    }
    
    // --- UI & VISIBILITY --- //
    
    function renderUI() {
        mainContainer.innerHTML = '';
        
        const header = document.createElement('div');
        Object.assign(header.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', () => {
            setTimeout(async () => {
                if (isDragging) return;
                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);
        });
        
        header.append(title, minimizeButton);
        mainContainer.appendChild(header);
        
        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');
            cardHeader.textContent = message.label;
            Object.assign(cardHeader.style, { fontWeight: 'bold', fontSize: '16px', borderBottom: '1px solid #555', paddingBottom: '10px' });
            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 editButton = document.createElement('button');
            editButton.textContent = 'Edit';
            Object.assign(editButton.style, { padding: '5px 10px', background: '#ffc107', color: 'black', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '12px' });
            editButton.onclick = () => openModal(message, index);
            leftButtons.append(copyButton, editButton);
            const deleteButton = document.createElement('button');
            deleteButton.textContent = 'Delete';
            Object.assign(deleteButton.style, { padding: '5px 10px', background: '#dc3545', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '12px' });
            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();
                }
            };
            toolbar.append(leftButtons, deleteButton);
            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, header, 'containerPosition');
    }
    
    async function toggleMinimize(minimize) {
        isMinimized = minimize;
        mainContainer.style.display = isMinimized ? 'none' : 'flex';
        floatingButton.style.display = isMinimized ? 'flex' : 'none';
        await GM_setValue('isMinimized', isMinimized);
    }

    // --- MAIN SCRIPT INITIALIZATION --- //

    async function initialize() {
        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', // High z-index
            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', // High z-index
            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', () => {
            setTimeout(async () => {
                if (isDragging) return;
                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);
    }

    initialize();

})();