Torn Quick Links

Floating draggable menu to manage and quickly access your favorite links.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Torn Quick Links
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Floating draggable menu to manage and quickly access your favorite links.
// @author       Omanpx [1906686] + Gemini
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- State Management ---
    let links = GM_getValue('navigator_links', [
        { id: 1, label: 'Armory', url: 'https://www.torn.com/factions.php?step=your&type=1#/tab=armoury', color: '#4285f4', emoji: '🔫', target: '_self' },
        { id: 2, label: 'Lottery', url: 'https://www.torn.com/page.php?sid=lottery', color: '#ff0000', emoji: '🍀', target: '_self' }
    ]);

    let isMenuOpen = false;
    let dragEnabled = GM_getValue('drag_enabled', false);
    let buttonPos = GM_getValue('button_pos', { top: '20px', left: '20px' });
    let lastToggleTime = 0; 

    // --- Styles ---
    const style = document.createElement('style');
    style.textContent = `
        #nav-pro-container {
            position: fixed;
            z-index: 999999;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            user-select: none;
            touch-action: none; /* Critical for mobile dragging */
        }

        #nav-pro-trigger {
            width: 50px;
            height: 50px;
            border-radius: 50%;
            background: #333;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            box-shadow: 0 4px 12px rgba(0,0,0,0.5);
            font-size: 24px;
            transition: transform 0.1s, border-color 0.2s;
            border: 2px solid transparent;
        }

        #nav-pro-trigger.drag-active {
            cursor: move;
            border-color: #4ade80;
        }

        #nav-pro-trigger:active { transform: scale(0.95); }

        #nav-pro-menu {
            position: absolute;
            width: 280px;
            max-height: 500px;
            background: #222;
            border-radius: 12px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.6);
            display: none;
            flex-direction: column;
            overflow: hidden;
            border: 1px solid #444;
            color: #eee;
        }

        .nav-pro-header {
            padding: 12px;
            background: #1a1a1a;
            border-bottom: 1px solid #444;
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-weight: bold;
        }

        .nav-pro-toolbar {
            padding: 8px 12px;
            background: #2a2a2a;
            border-bottom: 1px solid #444;
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 12px;
        }

        .nav-pro-list {
            overflow-y: auto;
            max-height: 300px;
            padding: 8px;
        }

        .nav-pro-item {
            display: flex;
            align-items: center;
            padding: 10px;
            margin-bottom: 4px;
            border-radius: 8px;
            text-decoration: none;
            color: #ddd;
            transition: background 0.2s;
            position: relative;
        }

        .nav-pro-item:hover { background: #333; color: #fff; }

        .nav-pro-color-dot {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            margin-right: 12px;
        }

        .nav-pro-emoji { margin-right: 8px; }

        .nav-pro-add-form {
            padding: 12px;
            border-top: 1px solid #444;
            background: #1a1a1a;
            font-size: 13px;
        }

        .nav-pro-input {
            width: 100%;
            padding: 6px 8px;
            margin-bottom: 6px;
            border: 1px solid #444;
            border-radius: 4px;
            box-sizing: border-box;
            background: #333;
            color: #eee;
        }

        .nav-pro-btn {
            width: 100%;
            padding: 8px;
            background: #444;
            color: white;
            border: 1px solid #555;
            border-radius: 4px;
            cursor: pointer;
            font-weight: 600;
        }

        .nav-pro-btn:hover { background: #555; }

        .nav-pro-delete {
            margin-left: auto;
            color: #ef4444;
            cursor: pointer;
            padding: 4px;
            font-size: 12px;
            opacity: 0.7;
        }

        .nav-pro-delete:hover { opacity: 1; }

        .switch {
            position: relative;
            display: inline-block;
            width: 34px;
            height: 20px;
        }

        .switch input { opacity: 0; width: 0; height: 0; }

        .slider {
            position: absolute;
            cursor: pointer;
            top: 0; left: 0; right: 0; bottom: 0;
            background-color: #444;
            transition: .4s;
            border-radius: 20px;
        }

        .slider:before {
            position: absolute;
            content: "";
            height: 14px; width: 14px;
            left: 3px; bottom: 3px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }

        input:checked + .slider { background-color: #2563eb; }
        input:checked + .slider:before { transform: translateX(14px); }
    `;
    document.head.appendChild(style);

    // --- UI Construction ---
    const container = document.createElement('div');
    container.id = 'nav-pro-container';
    container.style.top = buttonPos.top;
    container.style.left = buttonPos.left;

    container.innerHTML = `
        <div id="nav-pro-trigger" class="${dragEnabled ? 'drag-active' : ''}">🧭</div>
        <div id="nav-pro-menu">
            <div class="nav-pro-header">
                <span>Torn Quick Links</span>
                <button id="nav-pro-toggle-add" style="font-size:12px; cursor:pointer; background:#444; color:#fff; border:1px solid #666; border-radius:4px; padding:2px 6px;">+ Add</button>
            </div>
            <div class="nav-pro-toolbar">
                <span>Enable Dragging</span>
                <label class="switch">
                    <input type="checkbox" id="drag-toggle-switch" ${dragEnabled ? 'checked' : ''}>
                    <span class="slider"></span>
                </label>
            </div>
            <div class="nav-pro-list" id="nav-pro-items"></div>
            <div class="nav-pro-add-form" id="nav-pro-form" style="display:none;">
                <input type="text" id="add-label" class="nav-pro-input" placeholder="Label">
                <input type="text" id="add-url" class="nav-pro-input" placeholder="URL">
                <div style="display:flex; gap:5px; margin-bottom:10px;">
                    <input type="text" id="add-emoji" class="nav-pro-input" style="width:40%" placeholder="Emoji">
                    <input type="color" id="add-color" class="nav-pro-input" style="width:60%; height:30px; border:none;" value="#4285f4">
                </div>
                <label style="font-size:12px; display:block; margin-bottom:10px;">
                    <input type="checkbox" id="add-target"> Open in new tab
                </label>
                <button id="nav-pro-save" class="nav-pro-btn">Save Link</button>
            </div>
        </div>
    `;
    document.body.appendChild(container);

    const trigger = container.querySelector('#nav-pro-trigger');
    const menu = container.querySelector('#nav-pro-menu');
    const listContainer = container.querySelector('#nav-pro-items');
    const addForm = container.querySelector('#nav-pro-form');

    function renderLinks() {
        listContainer.innerHTML = '';
        links.forEach(link => {
            const item = document.createElement('div');
            item.className = 'nav-pro-item';
            item.innerHTML = `
                <div class="nav-pro-color-dot" style="background: ${link.color}"></div>
                <span class="nav-pro-emoji">${link.emoji || ''}</span>
                <span style="flex-grow:1; cursor:pointer;" class="nav-link-click">${link.label}</span>
                <span class="nav-pro-delete" data-id="${link.id}">✕</span>
            `;
            
            item.querySelector('.nav-link-click').onclick = (e) => {
                e.preventDefault();
                window.open(link.url, link.target || '_self');
                toggleMenu(false);
            };

            item.querySelector('.nav-pro-delete').onclick = (e) => {
                e.stopPropagation();
                links = links.filter(l => l.id !== link.id);
                GM_setValue('navigator_links', links);
                renderLinks();
            };

            listContainer.appendChild(item);
        });
    }

    function toggleMenu(force) {
        const now = Date.now();
        if (force === undefined && now - lastToggleTime < 300) return;
        lastToggleTime = now;

        isMenuOpen = force !== undefined ? force : !isMenuOpen;
        
        if (isMenuOpen) {
            menu.style.display = 'flex';
            const rect = container.getBoundingClientRect();
            const menuWidth = 280;
            const menuHeight = menu.offsetHeight || 400; // Estimate if not visible yet
            
            // Vertical positioning: default to bottom, switch to top if near bottom of screen
            if (rect.bottom + menuHeight > window.innerHeight) {
                menu.style.top = `-${menuHeight + 10}px`;
            } else {
                menu.style.top = '60px';
            }

            // Horizontal positioning: default to left-aligned, switch to right-aligned if near right edge
            if (rect.left + menuWidth > window.innerWidth) {
                menu.style.left = `-${menuWidth - 50}px`;
            } else {
                menu.style.left = '0';
            }
        } else {
            menu.style.display = 'none';
        }
    }

    // --- Interaction Logic (Fixed Flickering) ---
    let startX, startY, initialTop, initialLeft, hasMoved = false;

    const onPointerDown = (e) => {
        const ev = e.touches ? e.touches[0] : e;
        startX = ev.clientX;
        startY = ev.clientY;
        initialTop = container.offsetTop;
        initialLeft = container.offsetLeft;
        hasMoved = false;

        document.addEventListener('mousemove', onPointerMove);
        document.addEventListener('touchmove', onPointerMove, { passive: false });
        document.addEventListener('mouseup', onPointerUp);
        document.addEventListener('touchend', onPointerUp);
    };

    const onPointerMove = (e) => {
        const ev = e.touches ? e.touches[0] : e;
        const deltaX = ev.clientX - startX;
        const deltaY = ev.clientY - startY;

        if (Math.abs(deltaX) > 8 || Math.abs(deltaY) > 8) {
            hasMoved = true;
            if (dragEnabled) {
                container.style.left = `${initialLeft + deltaX}px`;
                container.style.top = `${initialTop + deltaY}px`;
                if (e.cancelable) e.preventDefault();
            }
        }
    };

    const onPointerUp = (e) => {
        document.removeEventListener('mousemove', onPointerMove);
        document.removeEventListener('touchmove', onPointerMove);
        document.removeEventListener('mouseup', onPointerUp);
        document.removeEventListener('touchend', onPointerUp);

        if (!hasMoved) {
            toggleMenu();
            if (e.cancelable) {
                e.preventDefault();
                e.stopImmediatePropagation();
            }
        } else if (dragEnabled) {
            GM_setValue('button_pos', { top: container.style.top, left: container.style.left });
        }
    };

    trigger.addEventListener('mousedown', onPointerDown);
    trigger.addEventListener('touchstart', onPointerDown, { passive: false });

    // --- Control Handlers ---
    container.querySelector('#drag-toggle-switch').onchange = (e) => {
        dragEnabled = e.target.checked;
        GM_setValue('drag_enabled', dragEnabled);
        trigger.classList.toggle('drag-active', dragEnabled);
    };

    container.querySelector('#nav-pro-toggle-add').onclick = (e) => {
        e.stopPropagation();
        addForm.style.display = addForm.style.display === 'none' ? 'block' : 'none';
    };

    container.querySelector('#nav-pro-save').onclick = (e) => {
        e.stopPropagation();
        const label = document.querySelector('#add-label').value;
        const url = document.querySelector('#add-url').value;
        const emoji = document.querySelector('#add-emoji').value;
        const color = document.querySelector('#add-color').value;
        const target = document.querySelector('#add-target').checked ? '_blank' : '_self';

        if (label && url) {
            links.push({
                id: Date.now(),
                label,
                url: url.startsWith('http') ? url : `https://${url}`,
                emoji,
                color,
                target
            });
            GM_setValue('navigator_links', links);
            renderLinks();
            document.querySelector('#add-label').value = '';
            document.querySelector('#add-url').value = '';
            addForm.style.display = 'none';
        }
    };

    renderLinks();
})();