Fab.com Library Category Manager (Multilingual)

A complete Tampermonkey script for creating, editing, and dragging categories on Fab.com's "My Library" page, and filtering assets by category. Supports EN, ZH-CN, JA, KO and hides the default Quixel panel on all language versions.

目前為 2025-08-25 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Fab.com Library Category Manager (Multilingual)
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  A complete Tampermonkey script for creating, editing, and dragging categories on Fab.com's "My Library" page, and filtering assets by category. Supports EN, ZH-CN, JA, KO and hides the default Quixel panel on all language versions.
// @author       Gemini & Gemini
// @match        https://www.fab.com/library*
// @match        https://www.fab.com/*/library*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- SECTION 0: I18N & LANGUAGE SUPPORT ---
    const i18n = {
        'en': {
            lang_code: 'en-US',
            quixel_link_text: "Browse Quixel Megascans",
            custom_categories_title: "Custom Categories",
            add_category_tooltip: "Add new category",
            show_all_button: "Show All",
            show_uncategorized_button: "Show Uncategorized",
            no_categories_placeholder: "No categories yet",
            rename_tooltip: "Rename",
            delete_tooltip: "Delete",
            delete_confirm: "Confirm?",
            new_category_placeholder: "Enter name and press Enter",
            set_category_tooltip: "Set Categories",
            toast_added_to: 'Added to "{categoryName}"',
            toast_removed_from: 'Removed from "{categoryName}"',
            toast_already_in: 'Already in "{categoryName}"',
            toast_error_category_exists: 'Error: Category "{name}" already exists.',
            toast_category_added: 'Category "{name}" was added.',
            toast_renamed_to: 'Renamed to "{newName}".',
            toast_category_deleted: 'Category "{name}" was deleted.',
            default_category_1: "Character Assets (Demo)",
            default_category_2: "Environment Props (Demo)",
            default_category_3: "Textures & Materials (Demo)",
        },
        'zh-cn': {
            lang_code: 'zh-CN',
            quixel_link_text: "浏览 Quixel Megascans",
            custom_categories_title: "自定义分类",
            add_category_tooltip: "添加分类",
            show_all_button: "显示全部",
            show_uncategorized_button: "显示未分类",
            no_categories_placeholder: "暂无分类",
            rename_tooltip: "重命名",
            delete_tooltip: "删除",
            delete_confirm: "确认?",
            new_category_placeholder: "输入分类名后按回车",
            set_category_tooltip: "设置分类",
            toast_added_to: '已添加到 "{categoryName}"',
            toast_removed_from: '已从 "{categoryName}" 中移除',
            toast_already_in: '"{categoryName}" 分类已存在',
            toast_error_category_exists: '错误:分类 "{name}" 已存在。',
            toast_category_added: '分类 "{name}" 已添加。',
            toast_renamed_to: '已重命名为 "{newName}"。',
            toast_category_deleted: '分类 "{name}" 已删除。',
            default_category_1: "角色资产 (测试)",
            default_category_2: "环境道具 (测试)",
            default_category_3: "贴图材质 (测试)",
        },
        'ja': {
            lang_code: 'ja-JP',
            quixel_link_text: "Quixel Megascansを閲覧",
            custom_categories_title: "カスタムカテゴリ",
            add_category_tooltip: "新しいカテゴリを追加",
            show_all_button: "すべて表示",
            show_uncategorized_button: "未分類を表示",
            no_categories_placeholder: "カテゴリがありません",
            rename_tooltip: "名前を変更",
            delete_tooltip: "削除",
            delete_confirm: "確認しますか?",
            new_category_placeholder: "名前を入力してEnter",
            set_category_tooltip: "カテゴリを設定",
            toast_added_to: '"{categoryName}" に追加しました',
            toast_removed_from: '"{categoryName}" から削除しました',
            toast_already_in: 'すでに "{categoryName}" に存在します',
            toast_error_category_exists: 'エラー:カテゴリ "{name}" はすでに存在します。',
            toast_category_added: 'カテゴリ "{name}" を追加しました。',
            toast_renamed_to: '"{newName}" に名前を変更しました。',
            toast_category_deleted: 'カテゴリ "{name}" を削除しました。',
            default_category_1: "キャラクターアセット (デモ)",
            default_category_2: "環境プロップ (デモ)",
            default_category_3: "テクスチャとマテリアル (デモ)",
        },
        'ko': {
            lang_code: 'ko-KR',
            quixel_link_text: "Quixel Megascans 찾아보기",
            custom_categories_title: "사용자 지정 카테고리",
            add_category_tooltip: "새 카테고리 추가",
            show_all_button: "모두 보기",
            show_uncategorized_button: "미분류 보기",
            no_categories_placeholder: "카테고리가 없습니다",
            rename_tooltip: "이름 바꾸기",
            delete_tooltip: "삭제",
            delete_confirm: "확인?",
            new_category_placeholder: "이름 입력 후 Enter",
            set_category_tooltip: "카테고리 설정",
            toast_added_to: '"{categoryName}"에 추가됨',
            toast_removed_from: '"{categoryName}"에서 제거됨',
            toast_already_in: '이미 "{categoryName}"에 있습니다',
            toast_error_category_exists: '오류: 카테고리 "{name}"이(가) 이미 존재합니다.',
            toast_category_added: '카테고리 "{name}"이(가) 추가되었습니다.',
            toast_renamed_to: '"{newName}"(으)로 이름이 변경되었습니다.',
            toast_category_deleted: '카테고리 "{name}"이(가) 삭제되었습니다.',
            default_category_1: "캐릭터 에셋 (데모)",
            default_category_2: "환경 소품 (데모)",
            default_category_3: "텍스처 및 재료 (데모)",
        }
    };

    function getLangInfo() {
        const path = window.location.pathname;
        const match = path.match(/^\/([a-z]{2}(-[a-z]{2})?)\//);
        const langCode = match ? match[1] : 'en';
        const translationKey = i18n.hasOwnProperty(langCode) ? langCode : 'en';
        const urlPrefix = (langCode === 'en') ? '' : `${langCode}/`;
        return { key: translationKey, prefix: urlPrefix };
    }

    const langInfo = getLangInfo();
    const currentLangKey = langInfo.key;

    function t(key, replacements = {}) {
        let text = i18n[currentLangKey]?.[key] || i18n['en']?.[key] || key;
        for (const placeholder in replacements) {
            text = text.replace(`{${placeholder}}`, replacements[placeholder]);
        }
        return text;
    }

    // --- SECTION 1: DATA & STATE MANAGEMENT ---
    let state = { categories: [], itemAssignments: {}, activeFilter: null };
    let draggedCategory = null;
    const STORAGE_KEYS = { CATEGORIES: 'fab_custom_categories_storage', ASSIGNMENTS: 'fab_item_assignments_storage' };
    function loadData() {
        state.categories = GM_getValue(STORAGE_KEYS.CATEGORIES, []);
        state.itemAssignments = GM_getValue(STORAGE_KEYS.ASSIGNMENTS, {});
        if (state.categories.length === 0) {
            state.categories = [
                { id: '1', name: t('default_category_1') },
                { id: '2', name: t('default_category_2') },
                { id: '3', name: t('default_category_3') }
            ];
            saveData();
        }
    }
    function saveData() { GM_setValue(STORAGE_KEYS.CATEGORIES, state.categories); GM_setValue(STORAGE_KEYS.ASSIGNMENTS, state.itemAssignments); }
    function showToast(message, isError = false) { const existingToast = document.getElementById('fab-custom-toast'); if (existingToast) existingToast.remove(); const toast = document.createElement('div'); toast.id = 'fab-custom-toast'; toast.textContent = message; toast.style.cssText = `position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); padding: 12px 20px; border-radius: 6px; color: white; font-weight: 500; background-color: ${isError ? '#D32F2F' : '#43A047'}; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 10000; opacity: 0; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);`; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '1'; toast.style.bottom = '30px'; }, 10); setTimeout(() => { toast.style.opacity = '0'; toast.style.bottom = '20px'; setTimeout(() => toast.remove(), 300); }, 3000); }

    // --- SECTION 2: LEFT PANEL UI & LOGIC ---
    function handleCategoryDragStart(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') { e.preventDefault(); return; } draggedCategory = e.currentTarget; e.dataTransfer.setData('text/plain--fab-category-id', draggedCategory.dataset.categoryId); e.dataTransfer.effectAllowed = 'move'; setTimeout(() => draggedCategory.classList.add('dragging-category'), 0); }
    function handleCategoryDragEnd(e) { if (!draggedCategory) return; draggedCategory.classList.remove('dragging-category'); document.querySelectorAll('.custom-category-item').forEach(item => { item.classList.remove('drop-indicator-top', 'drop-indicator-bottom'); }); draggedCategory = null; }
    function handleCategoryDragOver(e) { e.preventDefault(); const target = e.currentTarget; if (!draggedCategory || target === draggedCategory) return; const rect = target.getBoundingClientRect(); const midY = rect.top + rect.height / 2; if (e.clientY < midY) { target.classList.add('drop-indicator-top'); target.classList.remove('drop-indicator-bottom'); } else { target.classList.add('drop-indicator-bottom'); target.classList.remove('drop-indicator-top'); } }
    function handleCategoryDragEnter(e) { e.preventDefault(); const target = e.currentTarget; if (!draggedCategory || target === draggedCategory) return; target.classList.add('drag-over-sort'); }
    function handleCategoryDragLeave(e) { const target = e.currentTarget; if (!draggedCategory || target === draggedCategory) return; target.classList.remove('drop-indicator-top', 'drop-indicator-bottom', 'drag-over-sort'); }
    function handleCategoryDrop(e) { e.preventDefault(); e.stopPropagation(); const dropTarget = e.currentTarget; if (!draggedCategory || dropTarget === draggedCategory) return; const draggedId = e.dataTransfer.getData('text/plain--fab-category-id'); if (!draggedId) return; const targetId = dropTarget.dataset.categoryId; const draggedIndex = state.categories.findIndex(c => c.id === draggedId); const targetIndex = state.categories.findIndex(c => c.id === targetId); if (draggedIndex === -1 || targetIndex === -1) return; const [draggedItem] = state.categories.splice(draggedIndex, 1); const newTargetIndex = state.categories.findIndex(c => c.id === targetId); const rect = dropTarget.getBoundingClientRect(); const midY = rect.top + rect.height / 2; if (e.clientY < midY) { state.categories.splice(newTargetIndex, 0, draggedItem); } else { state.categories.splice(newTargetIndex + 1, 0, draggedItem); } saveData(); renderCategoryList(); }
    function renderCategoryList() {
        const listContainer = document.getElementById('custom-category-list-container');
        if (!listContainer) return;
        listContainer.innerHTML = '';
        if (state.categories.length === 0) { listContainer.innerHTML = `<li class="custom-category-item-placeholder">${t('no_categories_placeholder')}</li>`; } else {
            state.categories.forEach(category => {
                const li = document.createElement('li'); li.className = 'custom-category-item'; li.dataset.categoryId = category.id; li.setAttribute('draggable', 'true'); if (state.activeFilter === category.id) li.classList.add('is-active-filter');
                const nameSpan = document.createElement('span'); nameSpan.className = 'category-name'; nameSpan.textContent = category.name; nameSpan.addEventListener('click', () => applyFilter(category.id));
                const actionsDiv = document.createElement('div'); actionsDiv.className = 'category-actions'; actionsDiv.innerHTML = `<button class="edit-btn" title="${t('rename_tooltip')}">✏️</button><button class="delete-btn" title="${t('delete_tooltip')}">🗑️</button>`;
                li.appendChild(nameSpan); li.appendChild(actionsDiv); listContainer.appendChild(li);
                actionsDiv.querySelector('.edit-btn').addEventListener('click', () => handleRenameCategory(category.id));
                actionsDiv.querySelector('.delete-btn').addEventListener('click', (e) => handleDeleteCategory(e, category.id));
                li.addEventListener('dragover', (e) => { e.preventDefault(); if(e.dataTransfer.types.includes('text/plain--fab-category-id')) return; li.classList.add('drag-over'); });
                li.addEventListener('dragleave', () => { li.classList.remove('drag-over'); });
                li.addEventListener('drop', (e) => { e.preventDefault(); if(e.dataTransfer.types.includes('text/plain--fab-category-id')) return; li.classList.remove('drag-over'); const itemId = e.dataTransfer.getData('text/plain'); const card = document.querySelector(`div[data-gm-item-id="${itemId}"]`); if (card) { const assignments = state.itemAssignments[itemId] || []; if (!assignments.includes(category.id)) { toggleItemCategory(card, category.id, false); showToast(t('toast_added_to', { categoryName: category.name })); } else { showToast(t('toast_already_in', { categoryName: category.name }), true); } } });
                li.addEventListener('dragstart', handleCategoryDragStart); li.addEventListener('dragend', handleCategoryDragEnd); li.addEventListener('dragenter', handleCategoryDragEnter); li.addEventListener('dragleave', handleCategoryDragLeave); li.addEventListener('dragover', handleCategoryDragOver); li.addEventListener('drop', handleCategoryDrop);
            });
        }
    }
    function createNewCategory(input) { const name = input.value.trim(); if (name) { if (state.categories.some(c => c.name.toLowerCase() === name.toLowerCase())) { showToast(t('toast_error_category_exists', { name: name }), true); input.focus(); return; } state.categories.push({ id: Date.now().toString(), name: name }); saveData(); showToast(t('toast_category_added', { name: name })); } renderCategoryList(); }
    function handleRenameCategory(catId) { const li = document.querySelector(`.custom-category-item[data-category-id="${catId}"]`); const cat = state.categories.find(c => c.id === catId); if (!li || !cat) return; const oldName = cat.name; li.querySelector('.category-name').style.display = 'none'; const input = document.createElement('input'); input.type = 'text'; input.value = oldName; input.className = 'inline-category-input'; li.prepend(input); input.focus(); input.select(); const done = () => { const newName = input.value.trim(); if (newName && newName.toLowerCase() !== oldName.toLowerCase()) { if (state.categories.some(c => c.id !== catId && c.name.toLowerCase() === newName.toLowerCase())) { showToast(t('toast_error_category_exists', { name: newName }), true); input.focus(); return; } cat.name = newName; saveData(); showToast(t('toast_renamed_to', { newName: newName })); } renderCategoryList(); document.querySelectorAll('div[data-gm-item-id]').forEach(updateCardTag); }; input.addEventListener('keydown', (e) => { if (e.key === 'Enter') done(); }); input.addEventListener('blur', done); }
    function handleDeleteCategory(event, catId) { const btn = event.currentTarget; const cat = state.categories.find(c => c.id === catId); if (!cat) return; if (btn.classList.contains('confirm-delete')) { state.categories = state.categories.filter(c => c.id !== catId); Object.keys(state.itemAssignments).forEach(itemId => { state.itemAssignments[itemId] = state.itemAssignments[itemId].filter(id => id !== catId); if (state.itemAssignments[itemId].length === 0) { delete state.itemAssignments[itemId]; } }); saveData(); renderCategoryList(); showToast(t('toast_category_deleted', { name: cat.name })); document.querySelectorAll('div[data-gm-item-id]').forEach(updateCardTag); } else { btn.classList.add('confirm-delete'); btn.textContent = t('delete_confirm'); const timer = setTimeout(() => { btn.classList.remove('confirm-delete'); btn.textContent = '🗑️'; }, 3000); btn.onmouseleave = () => { clearTimeout(timer); btn.classList.remove('confirm-delete'); btn.textContent = '🗑️'; btn.onmouseleave = null; }; } }
    function handleAddCategory() { if (document.getElementById('new-category-input-li')) { document.querySelector('#new-category-input-li input').focus(); return; } const listContainer = document.getElementById('custom-category-list-container'); if (!listContainer) return; const placeholder = listContainer.querySelector('.custom-category-item-placeholder'); if (placeholder) placeholder.remove(); const li = document.createElement('li'); li.id = 'new-category-input-li'; li.className = 'custom-category-item'; const input = document.createElement('input'); input.type = 'text'; input.placeholder = t('new_category_placeholder'); input.className = 'inline-category-input'; li.appendChild(input); listContainer.prepend(li); input.focus(); input.addEventListener('keydown', function(e) { if (e.key === 'Enter') { createNewCategory(this); } else if (e.key === 'Escape') { renderCategoryList(); } }); input.addEventListener('blur', function() { setTimeout(() => { renderCategoryList(); }, 100); }); }
    function injectLeftPanelUI() {
        if (document.getElementById('custom-categories-container')) return;
        const container = document.createElement('div');
        container.id = 'custom-categories-container';
        container.innerHTML = `
            <a id="quixel-link-btn" href="https://www.fab.com/${langInfo.prefix}sellers/Quixel" target="_blank">${t('quixel_link_text')}</a>
            <div class="fabkit-Stack-root fabkit-Stack--column">
                <h2 class="custom-categories-title">
                    <span>${t('custom_categories_title')}</span>
                    <span id="add-category-btn" title="${t('add_category_tooltip')}">+</span>
                </h2>
                <div id="custom-category-controls">
                    <button id="clear-filter-btn">${t('show_all_button')}</button>
                    <button id="uncategorized-filter-btn">${t('show_uncategorized_button')}</button>
                </div>
                <nav class="fabkit-TreeView-root">
                    <ul id="custom-category-list-container" class="custom-category-list"></ul>
                </nav>
            </div>`;
        document.body.appendChild(container);
        document.getElementById('add-category-btn').addEventListener('click', handleAddCategory);
        document.getElementById('clear-filter-btn').addEventListener('click', clearFilter);
        document.getElementById('uncategorized-filter-btn').addEventListener('click', applyUncategorizedFilter);
        renderCategoryList();
    }


    // --- SECTION 3: RIGHT PANEL & CARD LOGIC ---
    const clickOutsideMenuHandler = (e) => { const menu = document.querySelector('.gm-category-popup-menu'); if (menu && !menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', clickOutsideMenuHandler, true); } };
    function updateCardTag(card) { const itemId = card.dataset.gmItemId; const tag = card.querySelector('.gm-category-tag'); if (!tag || !itemId) return; const assignedCategoryIds = state.itemAssignments[itemId]; if (assignedCategoryIds && assignedCategoryIds.length > 0) { const assignedNames = assignedCategoryIds.map(id => state.categories.find(c => c.id === id)?.name).filter(Boolean).join(', '); tag.textContent = assignedNames; tag.style.display = 'block'; } else { tag.textContent = ''; tag.style.display = 'none'; } }
    function toggleItemCategory(card, categoryId, suppressToast = false) { const itemId = card.dataset.gmItemId; if (!itemId) return; if (!state.itemAssignments[itemId]) state.itemAssignments[itemId] = []; const assignments = state.itemAssignments[itemId]; const categoryIndex = assignments.indexOf(categoryId); const categoryName = state.categories.find(c => c.id === categoryId)?.name || ''; if (categoryIndex > -1) { assignments.splice(categoryIndex, 1); if (!suppressToast) showToast(t('toast_removed_from', { categoryName: categoryName })); } else { assignments.push(categoryId); if (!suppressToast) showToast(t('toast_added_to', { categoryName: categoryName })); } if (assignments.length === 0) delete state.itemAssignments[itemId]; saveData(); updateCardTag(card); const menu = document.querySelector('.gm-category-popup-menu'); if (menu) { const menuItem = menu.querySelector(`li[data-category-id="${categoryId}"]`); if (menuItem) menuItem.classList.toggle('is-checked'); } }
    function showCategoryPopupMenu(event, card) { event.stopPropagation(); const existingMenu = document.querySelector('.gm-category-popup-menu'); document.removeEventListener('click', clickOutsideMenuHandler, true); if (existingMenu) { existingMenu.remove(); return; } const menu = document.createElement('ul'); menu.className = 'gm-category-popup-menu'; const itemId = card.dataset.gmItemId; const assignedCategoryIds = state.itemAssignments[itemId] || []; state.categories.forEach(cat => { const li = document.createElement('li'); li.className = 'gm-popup-item'; li.dataset.categoryId = cat.id; if (assignedCategoryIds.includes(cat.id)) li.classList.add('is-checked'); li.innerHTML = `<span class="gm-popup-checkmark">✔️</span> ${cat.name}`; li.addEventListener('click', (e) => { e.stopPropagation(); toggleItemCategory(card, cat.id); }); menu.appendChild(li); }); document.body.appendChild(menu); const btnRect = event.currentTarget.getBoundingClientRect(); menu.style.left = `${btnRect.left}px`; menu.style.top = `${btnRect.bottom + 5}px`; setTimeout(() => { document.addEventListener('click', clickOutsideMenuHandler, true); }, 0); }
    function applyFilter(categoryId) { if (state.activeFilter === categoryId) { clearFilter(); return; } state.activeFilter = categoryId; renderCategoryList(); }
    function clearFilter() { state.activeFilter = null; renderCategoryList(); }
    function applyUncategorizedFilter() { state.activeFilter = 'uncategorized'; renderCategoryList(); }
    function runFilter() { const allCards = document.querySelectorAll('div[data-gm-item-id]'); const clearBtn = document.getElementById('clear-filter-btn'); const uncatBtn = document.getElementById('uncategorized-filter-btn'); if (clearBtn) clearBtn.classList.toggle('is-active-filter', state.activeFilter === null); if (uncatBtn) uncatBtn.classList.toggle('is-active-filter', state.activeFilter === 'uncategorized'); if (state.activeFilter === null) { allCards.forEach(card => card.style.display = ''); return; } if (state.activeFilter === 'uncategorized') { allCards.forEach(card => { const itemId = card.dataset.gmItemId; const isAssigned = state.itemAssignments[itemId] && state.itemAssignments[itemId].length > 0; card.style.display = isAssigned ? 'none' : ''; }); return; } allCards.forEach(card => { const itemId = card.dataset.gmItemId; const assignments = state.itemAssignments[itemId] || []; if (assignments.includes(state.activeFilter)) { card.style.display = ''; } else { card.style.display = 'none'; } }); }

    function hideQuixelPanel() {
        const quixelLink = document.querySelector('a[href$="/sellers/Quixel"]');
        if (quixelLink) {
            const quixelPanel = quixelLink.closest('.HAfmzF_H');
            if (quixelPanel && quixelPanel.style.display !== 'none') {
                quixelPanel.style.display = 'none';
            }
        }
    }

    function findAndMarkCards() { document.querySelectorAll('div[class*="nTa5u2sc"]').forEach(card => { if (card.querySelector('a[href*="/listings/"]') && !card.dataset.gmProcessed) { card.dataset.gmProcessed = 'true'; const link = card.querySelector('a[href*="/listings/"]'); const itemId = link.href.split('/listings/')[1]; card.dataset.gmItemId = itemId; card.setAttribute('draggable', 'true'); card.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', itemId); setTimeout(() => card.classList.add('is-dragging'), 0); }); card.addEventListener('dragend', () => { card.classList.remove('is-dragging'); }); const uiContainer = document.createElement('div'); uiContainer.className = 'gm-ui-container'; const tag = document.createElement('div'); tag.className = 'gm-category-tag'; const button = document.createElement('button'); button.className = 'gm-category-button'; button.title = t('set_category_tooltip'); button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M10 4H4c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"></path></svg>`; button.addEventListener('click', (e) => showCategoryPopupMenu(e, card)); uiContainer.appendChild(tag); uiContainer.appendChild(button); card.appendChild(uiContainer); updateCardTag(card); } }); runFilter(); }

    // --- SECTION 4: STYLES & INITIALIZATION ---
    function injectGlobalStyles() {
        if (document.getElementById('fab-categorizer-global-styles')) return;
        const styleSheet = document.createElement('style');
        styleSheet.id = 'fab-categorizer-global-styles';
        styleSheet.textContent = `
            #custom-categories-container {
                position: fixed; top: 80px; bottom: 0; left: 10px; width: 280px; z-index: 0;
                background-color: transparent; border: none; box-shadow: none;
                padding: 10px; font-family: Inter, sans-serif; display: flex; flex-direction: column;
                pointer-events: none;
            }
            #custom-categories-container > * { pointer-events: auto; }
            #custom-categories-container .fabkit-Stack-root { flex: 1; display: flex; flex-direction: column; min-height: 0; }
            .fabkit-TreeView-root { flex: 1; overflow-y: auto; min-height: 0; }
            .custom-category-list { list-style: none; padding: 0; margin: 0; }
            #quixel-link-btn { display: block; text-align: center; margin-bottom: 10px; width: 100%; box-sizing: border-box; padding: 8px; background-color: var(--fab-palette-background-low, #2a2a2a); border: 1px solid var(--fab-palette-border, #333); color: var(--fab-palette-foreground-primary, #FFF); border-radius: 4px; cursor: pointer; transition: all 0.2s; text-decoration: none; font-size: 14px; }
            #quixel-link-btn:hover { background-color: #333; }
            .custom-categories-title { font-size: 0.875rem; font-weight: 700; color: var(--fab-palette-foreground-primary, #FFF); padding: 0 10px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; }
            #add-category-btn { cursor: pointer; color: var(--fab-palette-foreground-secondary, #AAA); font-size: 1.5rem; border-radius: 4px; transition: all 0.2s; line-height: 1; }
            #add-category-btn:hover { color: #FFF; background-color: #333; }
            #custom-category-controls { margin-bottom: 8px; display: flex; gap: 8px; }
            #custom-category-controls button { flex-grow: 1; padding: 8px; background-color: var(--fab-palette-background-low, #2a2a2a); border: 1px solid var(--fab-palette-border, #333); color: var(--fab-palette-foreground-primary, #FFF); border-radius: 4px; cursor: pointer; transition: all 0.2s; }
            #custom-category-controls button:hover { background-color: #333; }
            #custom-category-controls button.is-active-filter, .custom-category-item.is-active-filter { background-color: var(--fab-palette-primary-container, #004C99); font-weight: bold; border-color: var(--fab-palette-primary, #0078F2) !important; }
            .custom-category-item { color: var(--fab-palette-foreground-primary, #FFF); padding: 8px 10px; border-radius: 4px; min-height: 36px; display: flex; align-items: center; justify-content: space-between; transition: background-color 0.2s, outline 0.2s, border-top 0.2s, border-bottom 0.2s; border-top: 2px solid transparent; border-bottom: 2px solid transparent; }
            .custom-category-item .category-name { flex-grow: 1; cursor: pointer; }
            .custom-category-item:hover { background-color: var(--fab-palette-background-low, #2a2a2a); }
            .custom-category-item.drag-over { background-color: #004C99 !important; outline: 2px solid #0078F2; }
            .custom-category-item.dragging-category { opacity: 0.5; }
            .custom-category-item.drop-indicator-top { border-top: 2px solid #0078F2; }
            .custom-category-item.drop-indicator-bottom { border-bottom: 2px solid #0078F2; }
            .inline-category-input { width: 100%; background-color: #333; border: 1px solid #0078F2; color: #FFF; border-radius: 4px; padding: 5px 8px; outline: none; box-sizing: border-box; }
            .category-actions { display: none; }
            .custom-category-item:hover .category-actions { display: flex; }
            .category-actions button { background: none; border: none; cursor: pointer; margin-left: 8px; font-size: 16px; opacity: 0.6; transition: all 0.2s; padding: 0; }
            .category-actions button:hover { opacity: 1; }
            .delete-btn.confirm-delete { color: #D32F2F; font-size: 12px; font-weight: bold; width: 50px; }
            div[class*="nTa5u2sc"] { position: relative !important; }
            div[class*="nTa5u2sc"].is-dragging { opacity: 0.5; }
            .gm-ui-container { position: absolute !important; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; pointer-events: none; }
            .gm-category-tag { position: absolute; top: 8px; left: 8px; background-color: rgba(0, 0, 0, 0.7); color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; display: none; max-width: calc(100% - 50px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
            .gm-category-button { position: absolute; top: 5px; right: 5px; background-color: rgba(30, 30, 30, 0.8); border: 1px solid rgba(255, 255, 255, 0.2); color: white; border-radius: 5px; cursor: pointer; padding: 4px; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s; pointer-events: all; }
            div[class*="nTa5u2sc"]:hover .gm-category-button { opacity: 1; }
            .gm-category-button:hover { background-color: #333; }
            .gm-category-popup-menu { position: fixed; z-index: 10000; background: #2b2b2b; border: 1px solid #444; border-radius: 5px; list-style: none; padding: 5px 0; margin: 0; min-width: 180px; color: white; box-shadow: 0 5px 15px rgba(0,0,0,0.5); }
            .gm-popup-item { padding: 8px 12px; cursor: pointer; display: flex; align-items: center; }
            .gm-popup-item:hover { background: #4a4a4a; }
            .gm-popup-checkmark { color: #4CAF50; font-size: 1.2em; margin-right: 8px; visibility: hidden; }
            .gm-popup-item.is-checked .gm-popup-checkmark { visibility: visible; }
        `;
        document.head.appendChild(styleSheet);
    }

    function main() {
        loadData();
        injectGlobalStyles();
        injectLeftPanelUI();
        setInterval(() => {
            findAndMarkCards();
            hideQuixelPanel();
        }, 500);
    }
    main();

})();