US Card Forum User Tag

Tag users on US Card Forum with personal labels

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         US Card Forum User Tag
// @namespace    https://www.uscardforum.com/
// @version      1.2
// @description  Tag users on US Card Forum with personal labels
// @author       Your Name
// @match        https://www.uscardforum.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const STORAGE_KEY = 'uscardforum.user_tags.v1';
    const TAG_STYLE = 'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 4px; padding: 2px 8px; margin-left: 8px; font-size: 11px; font-weight: 600; display: inline-block; box-shadow: 0 2px 4px rgba(0,0,0,0.2); vertical-align: middle;';
    const BUTTON_STYLE = 'background: #667eea; color: white; border: none; border-radius: 4px; padding: 3px 8px; margin-left: 8px; cursor: pointer; font-size: 11px; transition: all 0.2s; vertical-align: middle;';

    // Storage functions
    function getTagMap() {
        const tagJsonStr = GM_getValue(STORAGE_KEY, '{}');
        return JSON.parse(tagJsonStr);
    }

    function saveTagMap(tagMap) {
        GM_setValue(STORAGE_KEY, JSON.stringify(tagMap));
    }

    function deleteTag(username) {
        const tagMap = getTagMap();
        delete tagMap[username];
        saveTagMap(tagMap);
    }

    // Create and display tag element
    function createTagElement(tagText) {
        const tagSpan = document.createElement('span');
        tagSpan.textContent = tagText;
        tagSpan.setAttribute('style', TAG_STYLE);
        tagSpan.className = 'user-custom-tag';
        return tagSpan;
    }

    // Create tag button
    function createTagButton(username, containerElement) {
        const tagMap = getTagMap();

        const button = document.createElement('button');
        button.textContent = '🏷️';
        button.className = 'tag-user-btn';
        button.setAttribute('style', BUTTON_STYLE);
        button.title = 'Tag this user';

        button.addEventListener('mouseenter', () => {
            button.style.background = '#5568d3';
        });
        button.addEventListener('mouseleave', () => {
            button.style.background = '#667eea';
        });

        button.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();

            const currentTag = tagMap[username] || '';
            const newTag = prompt(
                `Edit tag for user: ${username}\n\nEnter tags (comma-separated):\nExamples: "excellent debater", "helpful", "expert"`,
                currentTag
            );

            if (newTag !== null) {
                if (newTag.trim() === '') {
                    if (currentTag && confirm(`Remove all tags for ${username}?`)) {
                        deleteTag(username);
                        location.reload();
                    }
                } else {
                    tagMap[username] = newTag;
                    saveTagMap(tagMap);
                    location.reload();
                }
            }
        });

        return button;
    }

    // Add tags and button to a user element
    function processUserElement(userLink) {
        // Avoid processing twice
        if (userLink.hasAttribute('data-tag-processed')) {
            return;
        }
        userLink.setAttribute('data-tag-processed', 'true');

        // Skip if inside avatar elements
        if (userLink.closest('.post-avatar') || userLink.closest('.topic-avatar') || userLink.closest('.topic-poster') || userLink.closest('.topic-list-data')) {
            return;
        }

        const username = userLink.getAttribute('data-user-card') || userLink.textContent.trim();
        if (!username) return;

        const tagMap = getTagMap();
        const parentSpan = userLink.closest('.first.username') || userLink.parentElement;

        if (!parentSpan) return;

        // Add existing tags
        if (tagMap[username]) {
            const tags = tagMap[username].split(',').map(t => t.trim()).filter(t => t);
            tags.forEach(tag => {
                const tagElement = createTagElement(tag);
                parentSpan.appendChild(tagElement);
            });
        }

        // Add tag button
        const button = createTagButton(username, parentSpan);
        parentSpan.appendChild(button);
    }

    // Process all user links in posts
    function processPosts() {
        // Find all user links in post headers
        const userLinks = document.querySelectorAll('a[data-user-card][href^="/u/"]');
        userLinks.forEach(link => {
            processUserElement(link);
        });
    }

    // Process user profile page
    function processUserProfile() {
        // Profile page has different structure
        const profileHeader = document.querySelector('.user-profile-names .username');
        if (profileHeader && !profileHeader.hasAttribute('data-tag-processed')) {
            profileHeader.setAttribute('data-tag-processed', 'true');

            const username = profileHeader.textContent.trim();
            const tagMap = getTagMap();

            // Add tags
            if (tagMap[username]) {
                const tags = tagMap[username].split(',').map(t => t.trim()).filter(t => t);
                tags.forEach(tag => {
                    const tagElement = createTagElement(tag);
                    profileHeader.appendChild(tagElement);
                });
            }

            // Add button
            const button = createTagButton(username, profileHeader);
            profileHeader.appendChild(button);
        }
    }

    // Process user cards (popup when clicking username)
    function processUserCards() {
        const userCards = document.querySelectorAll('.user-card .names-link, .user-card .username');
        userCards.forEach(nameElement => {
            if (nameElement.hasAttribute('data-tag-processed')) return;
            nameElement.setAttribute('data-tag-processed', 'true');

            const username = nameElement.textContent.trim();
            const tagMap = getTagMap();

            // Add tags
            if (tagMap[username]) {
                const tags = tagMap[username].split(',').map(t => t.trim()).filter(t => t);
                tags.forEach(tag => {
                    const tagElement = createTagElement(tag);
                    nameElement.appendChild(tagElement);
                });
            }

            // Add button
            const button = createTagButton(username, nameElement);
            nameElement.appendChild(button);
        });
    }

    // Process topic lists
    function processTopicLists() {
        // Topic list poster names
        const posterLinks = document.querySelectorAll('.topic-list .posters a[data-user-card], .latest-topic-list-item a[data-user-card]');
        posterLinks.forEach(link => {
            if (link.hasAttribute('data-tag-processed')) return;
            link.setAttribute('data-tag-processed', 'true');

            const username = link.getAttribute('data-user-card') || link.getAttribute('title');
            if (!username) return;

            const tagMap = getTagMap();
            if (tagMap[username]) {
                const tags = tagMap[username].split(',').map(t => t.trim()).filter(t => t);
                tags.forEach(tag => {
                    const tagElement = createTagElement(tag);
                    tagElement.style.marginLeft = '4px';
                    link.parentElement.appendChild(tagElement);
                });
            }
        });
    }

    // Main processing function
    function processAll() {
        processPosts();
        processUserProfile();
        processUserCards();
        processTopicLists();
    }

    // Observer for dynamic content
    const observer = new MutationObserver((mutations) => {
        // Debounce to avoid excessive processing
        clearTimeout(window.tagProcessTimeout);
        window.tagProcessTimeout = setTimeout(() => {
            processAll();
        }, 100);
    });

    // Initialize
    function init() {
        console.log('US Card Forum User Tag script initialized');
        processAll();

        // Start observing
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        console.log('Found', document.querySelectorAll('a[data-user-card][href^="/u/"]').length, 'user links');
    }

    // Wait for page to load
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();