Claude Usage Tracker v2

Floating panel to track Claude usage limits and reset times. Drag anywhere to move. Minimized by default.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Claude Usage Tracker v2
// @namespace    usage-and-quick-settings-of-claude
// @author       Yalums
// @version      2
// @description  Floating panel to track Claude usage limits and reset times. Drag anywhere to move. Minimized by default.
// @match        https://claude.ai/*
// @grant        none
// @run-at       document-start
// @license      GNU General Public License v3.0
// ==/UserScript==

(function() {
    'use strict';

    // v2: Smoother dragging + single click to toggle (no double click needed)

    function resetPositionIfNeeded() {
        const pos = JSON.parse(localStorage.getItem('claudePanel_position') || '{}');
        const left = parseInt(pos.left) || 0;
        const top = parseInt(pos.top) || 0;

        if (left > window.innerWidth - 50 || top > window.innerHeight - 50 || left < 0 || top < 0) {
            localStorage.setItem('claudePanel_position', JSON.stringify({left: "80px", top: "100px"}));
        }
    }

    const storedMinimized = localStorage.getItem('claudePanel_minimized');
    let panelState = {
        isMinimized: storedMinimized !== 'false',
        position: JSON.parse(localStorage.getItem('claudePanel_position') || '{"left":"80px","top":"100px"}')
    };

    async function getUsageData() {
        try {
            const orgsResponse = await fetch('/api/organizations', { credentials: 'include' });
            const orgs = await orgsResponse.json();
            const orgId = orgs[0]?.uuid;
            if (!orgId) return null;
            const usageResponse = await fetch(`/api/organizations/${orgId}/usage`, { credentials: 'include' });
            return await usageResponse.json();
        } catch (err) {
            return null;
        }
    }

    function formatResetTime(isoTime) {
        if (!isoTime) return 'N/A';
        const diff = new Date(isoTime) - new Date();
        const minutes = Math.floor(diff / 60000);
        const hours = Math.floor(diff / 3600000);
        const days = Math.floor(diff / 86400000);
        if (minutes < 1) return 'soon';
        if (minutes < 60) return `${minutes}m`;
        if (hours < 24) return `${hours}h`;
        return `${days}d`;
    }

    function injectStyles() {
        if (document.getElementById('claude-panel-styles')) return;

        const style = document.createElement('style');
        style.id = 'claude-panel-styles';
        style.textContent = `
            #claude-control-panel {
                position: fixed !important;
                z-index: 2147483647 !important;
                background: linear-gradient(145deg, #6366f1 0%, #7c3aed 100%) !important;
                border: none !important;
                border-radius: 14px !important;
                box-shadow: 0 4px 24px rgba(99, 102, 241, 0.4) !important;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
                color: #ffffff !important;
                overflow: hidden !important;
                display: block !important;
                visibility: visible !important;
                opacity: 1 !important;
                pointer-events: auto !important;
                touch-action: none !important;
                user-select: none !important;
            }

            #claude-control-panel.dragging {
                cursor: grabbing !important;
                box-shadow: 0 12px 40px rgba(99, 102, 241, 0.6) !important;
                opacity: 0.9 !important;
            }

            /* MINIMIZED STATE */
            #claude-control-panel.minimized {
                width: 46px !important;
                height: 46px !important;
                cursor: pointer !important;
                border-radius: 13px !important;
                padding: 0 !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
            }

            #claude-control-panel.minimized:not(.dragging):hover {
                box-shadow: 0 6px 28px rgba(99, 102, 241, 0.7) !important;
                transform: scale(1.06) !important;
            }

            #claude-control-panel.minimized .panel-expanded {
                display: none !important;
            }

            #claude-control-panel.minimized .panel-icon {
                display: flex !important;
                font-size: 20px !important;
                cursor: pointer !important;
                align-items: center !important;
                justify-content: center !important;
                width: 100% !important;
                height: 100% !important;
            }

            /* EXPANDED STATE */
            #claude-control-panel:not(.minimized) {
                width: 160px !important;
                cursor: grab !important;
            }

            #claude-control-panel:not(.minimized) .panel-icon {
                display: none !important;
            }

            .panel-expanded {
                display: flex !important;
                flex-direction: column !important;
            }

            /* Header */
            .panel-header-row {
                display: flex !important;
                align-items: center !important;
                justify-content: space-between !important;
                padding: 8px 10px !important;
                background: rgba(0, 0, 0, 0.15) !important;
                border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
                cursor: pointer !important;
            }

            .panel-header-row:hover {
                background: rgba(0, 0, 0, 0.25) !important;
            }

            .panel-title {
                font-size: 11px !important;
                font-weight: 700 !important;
                display: flex !important;
                align-items: center !important;
                gap: 5px !important;
            }

            .panel-btns {
                display: flex !important;
                gap: 4px !important;
            }

            .panel-btn-sm {
                background: rgba(255, 255, 255, 0.15) !important;
                border: none !important;
                border-radius: 5px !important;
                width: 22px !important;
                height: 22px !important;
                cursor: pointer !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
                color: #ffffff !important;
                font-size: 12px !important;
                transition: background 0.15s ease !important;
            }

            .panel-btn-sm:hover {
                background: rgba(255, 255, 255, 0.3) !important;
            }

            /* Content */
            .panel-content-area {
                padding: 8px 10px 10px !important;
                display: flex !important;
                flex-direction: column !important;
                gap: 6px !important;
            }

            /* Usage items - compact rows */
            .usage-row-item {
                display: flex !important;
                align-items: center !important;
                gap: 8px !important;
            }

            .usage-name {
                font-size: 10px !important;
                font-weight: 600 !important;
                width: 32px !important;
                opacity: 0.9 !important;
            }

            .usage-bar-wrap {
                flex: 1 !important;
                height: 7px !important;
                background: rgba(255, 255, 255, 0.25) !important;
                border-radius: 4px !important;
                overflow: hidden !important;
            }

            .usage-bar-inner {
                height: 100% !important;
                background: #34d399 !important;
                border-radius: 4px !important;
                transition: width 0.3s ease !important;
            }

            .usage-bar-inner.warning {
                background: #fbbf24 !important;
            }

            .usage-bar-inner.danger {
                background: #f87171 !important;
            }

            .usage-info {
                font-size: 9px !important;
                text-align: right !important;
                min-width: 38px !important;
                opacity: 0.85 !important;
            }

            .usage-pct {
                font-weight: 700 !important;
            }

            .usage-time {
                opacity: 0.7 !important;
                font-size: 8px !important;
            }

            body.panel-dragging {
                user-select: none !important;
                cursor: grabbing !important;
            }

            body.panel-dragging * {
                cursor: grabbing !important;
            }
        `;

        if (document.head) {
            document.head.appendChild(style);
        } else if (document.documentElement) {
            document.documentElement.appendChild(style);
        }
    }

    function togglePanel(panel) {
        panel.classList.toggle('minimized');
        panelState.isMinimized = panel.classList.contains('minimized');
        localStorage.setItem('claudePanel_minimized', panelState.isMinimized ? 'true' : 'false');
    }

    function createPanel() {
        const existing = document.getElementById('claude-control-panel');
        if (existing) existing.remove();

        resetPositionIfNeeded();
        panelState.position = JSON.parse(localStorage.getItem('claudePanel_position') || '{"left":"80px","top":"100px"}');

        const panel = document.createElement('div');
        panel.id = 'claude-control-panel';
        panel.className = panelState.isMinimized ? 'minimized' : '';

        panel.innerHTML = `
            <div class="panel-icon">📊</div>
            <div class="panel-expanded">
                <div class="panel-header-row" id="header-row">
                    <span class="panel-title">📊 Usage</span>
                    <div class="panel-btns">
                        <button class="panel-btn-sm" id="refresh-btn" title="Refresh">🔄</button>
                    </div>
                </div>
                <div class="panel-content-area" id="usage-content">
                    <div style="font-size: 10px; opacity: 0.8; text-align: center; padding: 4px;">Loading...</div>
                </div>
            </div>
        `;

        const left = parseInt(panelState.position.left) || 80;
        const top = parseInt(panelState.position.top) || 100;

        panel.style.left = left + 'px';
        panel.style.top = top + 'px';

        const container = document.body || document.documentElement;
        container.appendChild(panel);

        // Refresh button
        panel.querySelector('#refresh-btn').addEventListener('click', (e) => {
            e.stopPropagation();
            const content = panel.querySelector('#usage-content');
            content.innerHTML = '<div style="font-size: 10px; opacity: 0.8; text-align: center; padding: 4px;">...</div>';
            updatePanelContent(panel);
        });

        makeDraggable(panel);
        return panel;
    }

    async function updatePanelContent(panel) {
        const content = panel.querySelector('#usage-content');
        if (!content) return;

        const usageData = await getUsageData();

        if (!usageData) {
            content.innerHTML = '<div style="font-size: 10px; opacity: 0.8; text-align: center; padding: 4px;">❌ Error</div>';
            return;
        }

        let html = '';

        if (usageData.five_hour) {
            const percent = usageData.five_hour.utilization || 0;
            const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
            const time = formatResetTime(usageData.five_hour.resets_at);
            html += `
                <div class="usage-row-item">
                    <span class="usage-name">5hr</span>
                    <div class="usage-bar-wrap"><div class="usage-bar-inner ${barClass}" style="width: ${percent}%"></div></div>
                    <span class="usage-info"><span class="usage-pct">${percent}%</span> <span class="usage-time">${time}</span></span>
                </div>
            `;
        }

        if (usageData.seven_day) {
            const percent = usageData.seven_day.utilization || 0;
            const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
            const time = formatResetTime(usageData.seven_day.resets_at);
            html += `
                <div class="usage-row-item">
                    <span class="usage-name">7day</span>
                    <div class="usage-bar-wrap"><div class="usage-bar-inner ${barClass}" style="width: ${percent}%"></div></div>
                    <span class="usage-info"><span class="usage-pct">${percent}%</span> <span class="usage-time">${time}</span></span>
                </div>
            `;
        }

        if (usageData.seven_day_opus) {
            const percent = usageData.seven_day_opus.utilization || 0;
            const barClass = percent > 80 ? 'danger' : percent > 60 ? 'warning' : '';
            const time = formatResetTime(usageData.seven_day_opus.resets_at);
            html += `
                <div class="usage-row-item">
                    <span class="usage-name">Opus</span>
                    <div class="usage-bar-wrap"><div class="usage-bar-inner ${barClass}" style="width: ${percent}%"></div></div>
                    <span class="usage-info"><span class="usage-pct">${percent}%</span> <span class="usage-time">${time}</span></span>
                </div>
            `;
        }

        content.innerHTML = html || '<div style="font-size: 10px; opacity: 0.8; text-align: center;">No data</div>';
    }

    function makeDraggable(element) {
        let isDragging = false;
        let hasMoved = false;
        let offsetX = 0, offsetY = 0;
        let startLeft = 0, startTop = 0;

        function getPointerPosition(e) {
            if (e.touches && e.touches.length > 0) {
                return { x: e.touches[0].clientX, y: e.touches[0].clientY };
            }
            return { x: e.clientX, y: e.clientY };
        }

        function dragStart(e) {
            // Don't drag from buttons
            if (e.target.closest('.panel-btn-sm')) return;

            const pos = getPointerPosition(e);
            const rect = element.getBoundingClientRect();

            offsetX = pos.x - rect.left;
            offsetY = pos.y - rect.top;
            startLeft = rect.left;
            startTop = rect.top;

            isDragging = true;
            hasMoved = false;

            element.classList.add('dragging');
            document.body.classList.add('panel-dragging');

            e.preventDefault();
        }

        function drag(e) {
            if (!isDragging) return;
            e.preventDefault();

            const pos = getPointerPosition(e);

            let newX = pos.x - offsetX;
            let newY = pos.y - offsetY;

            // Check if moved more than 5px (threshold for "actually dragging")
            if (Math.abs(newX - startLeft) > 5 || Math.abs(newY - startTop) > 5) {
                hasMoved = true;
            }

            // Keep within viewport
            const maxX = window.innerWidth - element.offsetWidth;
            const maxY = window.innerHeight - element.offsetHeight;
            newX = Math.max(0, Math.min(newX, maxX));
            newY = Math.max(0, Math.min(newY, maxY));

            // Direct position update (smooth on modern browsers)
            element.style.left = newX + 'px';
            element.style.top = newY + 'px';
        }

        function dragEnd(e) {
            if (!isDragging) return;
            isDragging = false;

            element.classList.remove('dragging');
            document.body.classList.remove('panel-dragging');

            // Save position
            panelState.position = { left: element.style.left, top: element.style.top };
            localStorage.setItem('claudePanel_position', JSON.stringify(panelState.position));

            // SINGLE CLICK TO TOGGLE - if didn't move, toggle the panel
            if (!hasMoved) {
                togglePanel(element);
            }
        }

        element.addEventListener('mousedown', dragStart);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', dragEnd);

        element.addEventListener('touchstart', dragStart, { passive: false });
        document.addEventListener('touchmove', drag, { passive: false });
        document.addEventListener('touchend', dragEnd);
    }

    function init() {
        injectStyles();
        const panel = createPanel();
        updatePanelContent(panel);
        setInterval(() => updatePanelContent(panel), 60000);
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        setTimeout(init, 500);
    }

    document.addEventListener('DOMContentLoaded', () => {
        if (!document.getElementById('claude-control-panel')) {
            setTimeout(init, 500);
        }
    });

    window.addEventListener('load', () => {
        if (!document.getElementById('claude-control-panel')) {
            setTimeout(init, 1000);
        }
    });

    setTimeout(() => {
        if (!document.getElementById('claude-control-panel')) {
            init();
        }
    }, 3000);

})();