JAV Manager

This script reads code and actress data from GM_setValue. Provides multi-source loading/hiding of actress info, reorganizes management panel layout, supports multi-site actress display, and manual invocation with buttons and hotkeys.

Stan na 04-01-2025. Zobacz najnowsza wersja.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         JAV Manager
// @namespace    https://example.com
// @version      1.0
// @description  This script reads code and actress data from GM_setValue. Provides multi-source loading/hiding of actress info, reorganizes management panel layout, supports multi-site actress display, and manual invocation with buttons and hotkeys.
// @match        https://jinjier.art/sql*
// @match        https://javdb.com/*
// @exclude      https://javdb.com/actors/*
// @exclude      https://javdb.com/v/*
// @match        https://www.javlibrary.com/cn/vl_bestrated.php*
// @match        https://www.javlibrary.com/cn/vl_mostwanted.php*
// @match        https://www.javlibrary.com/cn/vl_update.php*
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      javbus.com
// @connect      jable.tv
// @connect      javmenu.com
// @connect      javdb.com
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    /*******************************
     * Block 1
     * 1. Constant definitions
     * 2. Utility functions
     * 3. Style injection
     * 4. Create section function
     * 5. Keyword management panel (UI skeleton)
     *******************************/

    // ===== 1. Constant definitions =====
    const STORAGE_KEY = 'sql_filter_keywords'; // 屏蔽
    const FAVORITES_KEY = 'sql_favorite_keywords'; // 喜爱
    const KNOWN_KEY = 'sql_known_keywords'; // 认识
    const WATCHED_KEY = 'sql_watched_keywords'; // 看过
    const SQL_STORAGE_KEY = 'sql_last_executed'; // 上次执行的SQL
    const FILTER_TOGGLE_KEY = 'sql_filter_enabled'; // 屏蔽功能开关
    const SORT_PRIORITY_KEY = 'sql_sort_priority_enabled'; // 按照priority排序开关
    const SORT_ACTOR_KEY = 'sql_sort_actor_enabled'; // 按照演员信息排序开关
    const NUMBER_ACTOR_STORAGE_KEY = 'sql_number_actor_info'; // 番号与演员信息存储键名
    const ACTORS_COLUMN_SHOULD_INSERT = 'actors_column_should_insert';
    const ACTORS_COLUMN_INSERTED_KEY = 'actors_column_inserted'; // 存储演员列是否已插入

    let isSelectingText = false; // Mark if the user is currently selecting text

    // ===== 2. Utility functions =====
    // Remove brackets and their contents from actress name
    function stripParentheses(text) {
        return text.replace(/\s*([^)]*)/g, '').replace(/\s*\([^)]*\)/g, '').trim();
    }

    // General async GM_xmlhttpRequest
    function gmRequest(details) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                ...details,
                onload: function (response) {
                    resolve(response);
                },
                onerror: function (error) {
                    reject(error);
                }
            });
        });
    }

    // ===== 3. Style injection =====
    function addCustomStyles() {
        const style = document.createElement('style');
        style.innerHTML = `
            html {
                width: 100% !important;
                margin: 0 !important;
            }
            @media screen and (min-width:800px) {
                html {
                    width: 100% !important;
                    margin: 0 !important;
                }
                body {
                    border: none !important;
                    padding: 0 20px !important;
                    box-shadow: none !important;
                    border-radius: 0 !important;
                }
            }
            /* Management panel style */
            #keywordManager {
                max-height: 50px;
                transition: max-height 0.3s ease-out;
                overflow: hidden;
                position: fixed;
                top: 20px;
                right: 20px;
                background-color: #ffffff;
                border: 1px solid #ccc;
                padding: 20px;
                z-index: 1000;
                width: 400px;
                box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
                border-radius: 8px;
                font-family: Arial, sans-serif;
                font-size: 14px;
                line-height: 1.5;
                color: #333;
            }
            #keywordManager.expanded {
                max-height: 800px; /* Large enough to fit content */
            }
            /* Progress bar style */
            #progressOverlay {
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0, 0, 0, 0.5);
                display: flex;
                justify-content: center;
                align-items: center;
                z-index: 3000;
            }
            #progressBarContainer {
                width: 80%;
                background: #fff;
                padding: 20px;
                border-radius: 8px;
                text-align: center;
            }
            #progressBar {
                width: 100%;
                background: #e0e0e0;
                border-radius: 4px;
                overflow: hidden;
                margin-top: 10px;
            }
            #progressBar div {
                height: 20px;
                width: 0;
                background: #28a745;
                transition: width 0.3s;
            }
            /* Floating button style */
            .floating-button {
                position: fixed;
                display: none;
                z-index: 1000;
                padding: 5px 10px;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 12px;
            }
            #filterButton {
                background-color: #dc3545;
                color: #fff;
            }
            #filterButton:hover {
                background-color: #c82333;
            }
            #favoriteButton {
                background-color: #28a745;
                color: #fff;
            }
            #favoriteButton:hover {
                background-color: #218838;
            }
            #knownButton {
                background-color: #17a2b8;
                color: #fff;
            }
            #knownButton:hover {
                background-color: #117a8b;
            }
            #watchedButton {
                background-color: #007bff;
                color: #fff;
            }
            #watchedButton:hover {
                background-color: #0056b3;
            }
            /* Settings button style */
            #settingsButton {
                padding: 5px 10px;
                background-color: #ffc107;
                color: #fff;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 12px;
                position: absolute;
                bottom: 20px;
                right: 20px;
            }
            #settingsButton:hover {
                background-color: #e0a800;
            }
            /* Settings modal style */
            #settingsModal {
                position: fixed; /* changed to fixed positioning */
                background-color: #fff;
                padding: 20px;
                border: 1px solid #ccc;
                border-radius: 8px;
                z-index: 4000;
                box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
                display: none;
                width: 250px;
                max-width: 90vw;
                max-height: 90vh;
            }
            #settingsModal h4 {
                margin-top: 0;
            }
            #settingsModal label {
                display: block;
                margin-bottom: 10px;
            }
            #settingsModal button.close-settings {
                padding: 5px 10px;
                margin-top: 10px;
                background-color: #dc3545;
                color: #fff;
                border: none;
                border-radius: 4px;
                cursor: pointer;
            }
            #settingsModal button.close-settings:hover {
                background-color: #c82333;
            }
        `;
        document.head.appendChild(style);
    }
    addCustomStyles(); // Inject styles immediately

    // ===== 4. Create section function =====
    function createSection(sectionId, sectionTitle, storageKey) {
        let section = document.createElement('div');
        section.id = sectionId;

        let title = document.createElement('h4');
        title.textContent = sectionTitle;
        title.style.marginTop = '10px';
        title.style.marginBottom = '5px';
        title.style.color = '#444';
        section.appendChild(title);

        let inputContainer = document.createElement('div');
        inputContainer.style.display = 'flex';
        inputContainer.style.marginBottom = '10px';

        let input = document.createElement('input');
        input.type = 'text';
        input.placeholder = '添加关键词';
        input.style.flex = '1';
        input.style.padding = '5px';
        input.style.border = '1px solid #ccc';
        input.style.borderRadius = '4px';

        let addButton = document.createElement('button');
        addButton.textContent = '添加';
        addButton.style.padding = '5px 10px';
        addButton.style.marginLeft = '5px';
        addButton.style.backgroundColor = '#28a745';
        addButton.style.color = '#fff';
        addButton.style.border = 'none';
        addButton.style.borderRadius = '4px';
        addButton.style.cursor = 'pointer';
        addButton.onmouseover = function () {
            addButton.style.backgroundColor = '#218838';
        };
        addButton.onmouseout = function () {
            addButton.style.backgroundColor = '#28a745';
        };
        addButton.onclick = function (event) {
            event.stopPropagation(); // Prevent triggering panel toggle
            let keyword = input.value.trim();
            if (keyword) {
                addKeyword(keyword, storageKey, sectionId);
                input.value = '';
            }
        };

        inputContainer.appendChild(input);
        inputContainer.appendChild(addButton);
        section.appendChild(inputContainer);

        // Keyword list
        let list = document.createElement('ul');
        list.style.listStyleType = 'none';
        list.style.padding = '0';
        list.style.maxHeight = '200px';
        list.style.overflowY = 'auto';
        section.appendChild(list);

        // Bottom container, containing batch import button and feature toggle
        let bottomContainer = document.createElement('div');
        bottomContainer.style.display = 'flex';
        bottomContainer.style.alignItems = 'center';

        // Batch import button
        let batchImportButton = document.createElement('button');
        batchImportButton.textContent = '批量导入';
        batchImportButton.style.padding = '5px 10px';
        batchImportButton.style.backgroundColor = '#17a2b8';
        batchImportButton.style.color = '#fff';
        batchImportButton.style.border = 'none';
        batchImportButton.style.borderRadius = '4px';
        batchImportButton.style.cursor = 'pointer';
        batchImportButton.style.marginRight = '10px';
        batchImportButton.onmouseover = function () {
            batchImportButton.style.backgroundColor = '#117a8b';
        };
        batchImportButton.onmouseout = function () {
            batchImportButton.style.backgroundColor = '#17a2b8';
        };
        batchImportButton.onclick = function (event) {
            event.stopPropagation(); // Prevent triggering panel toggle
            showBatchImportModal(storageKey, sectionId);
        };
        bottomContainer.appendChild(batchImportButton);

        // If it is the "屏蔽" section, add an enable filter toggle
        if (storageKey === STORAGE_KEY) {
            let filterToggleContainer = document.createElement('div');
            filterToggleContainer.style.display = 'flex';
            filterToggleContainer.style.alignItems = 'center';
            filterToggleContainer.style.marginLeft = '10px';

            let filterToggleSwitch = document.createElement('input');
            filterToggleSwitch.type = 'checkbox';
            filterToggleSwitch.checked = getFilterEnabled();
            filterToggleSwitch.onchange = function () {
                GM_setValue(FILTER_TOGGLE_KEY, filterToggleSwitch.checked);
                modifyPage(); // Re-process the page
            };
            filterToggleContainer.appendChild(filterToggleSwitch);

            let filterToggleLabel = document.createElement('label');
            filterToggleLabel.textContent = ' 启用屏蔽';
            filterToggleLabel.style.marginLeft = '5px';
            filterToggleContainer.appendChild(filterToggleLabel);

            bottomContainer.appendChild(filterToggleContainer);
        }
        section.appendChild(bottomContainer);
        return section;
    }

    // ===== 5. Keyword management panel (UI skeleton) =====
    function createKeywordManager() {
        let manager = document.createElement('div');
        manager.id = 'keywordManager';

        // Title bar
        let title = document.createElement('div');
        title.style.display = 'flex';
        title.style.justifyContent = 'space-between';
        title.style.alignItems = 'center';
        title.style.cursor = 'pointer';

        let titleText = document.createElement('h3');
        titleText.textContent = '管理面板';
        titleText.style.fontSize = '16px';
        titleText.style.color = '#444';
        titleText.style.margin = '0';

        let titleRightContainer = document.createElement('div');
        titleRightContainer.style.display = 'flex';
        titleRightContainer.style.alignItems = 'center';

        // Export button
        let exportButton = document.createElement('button');
        exportButton.textContent = '导出关键词';
        exportButton.id = 'exportKeywordsButton';
        exportButton.style.padding = '5px 10px';
        exportButton.style.backgroundColor = '#007bff';
        exportButton.style.color = '#fff';
        exportButton.style.border = 'none';
        exportButton.style.borderRadius = '4px';
        exportButton.style.cursor = 'pointer';
        exportButton.style.fontSize = '12px';
        exportButton.style.marginRight = '10px';
        exportButton.onmouseover = function () {
            exportButton.style.backgroundColor = '#0056b3';
        };
        exportButton.onmouseout = function () {
            exportButton.style.backgroundColor = '#007bff';
        };
        exportButton.onclick = function (event) {
            event.stopPropagation();
            exportKeywordsToCSV();
        };
        titleRightContainer.appendChild(exportButton);

        // Load/hide actress info button
        let loadActorsButton = document.createElement('button');
        loadActorsButton.textContent = '加载演员信息';
        loadActorsButton.id = 'loadActorsButton';
        loadActorsButton.style.padding = '5px 10px';
        loadActorsButton.style.backgroundColor = '#6c757d';
        loadActorsButton.style.color = '#fff';
        loadActorsButton.style.border = 'none';
        loadActorsButton.style.borderRadius = '4px';
        loadActorsButton.style.cursor = 'pointer';
        loadActorsButton.style.fontSize = '12px';
        loadActorsButton.style.marginLeft = '0px';
        loadActorsButton.onmouseover = function () {
            loadActorsButton.style.backgroundColor = '#5a6268';
        };
        loadActorsButton.onmouseout = function () {
            loadActorsButton.style.backgroundColor = '#6c757d';
        };
        loadActorsButton.onclick = function (event) {
            event.stopPropagation();
            toggleActorsColumn();
        };
        titleRightContainer.appendChild(loadActorsButton);

        let toggleIcon = document.createElement('span');
        toggleIcon.textContent = '▼';
        toggleIcon.style.fontSize = '18px';
        toggleIcon.style.marginLeft = '10px';
        titleRightContainer.appendChild(toggleIcon);

        title.appendChild(titleText);
        title.appendChild(titleRightContainer);
        title.onclick = toggleManager;
        manager.appendChild(title);

        // Navigation bar
        let nav = document.createElement('div');
        nav.style.display = 'flex';
        nav.style.marginTop = '10px';

        // Create navigation buttons
        let favoriteTab = createNavButton('喜爱', 'favorite');
        let knownTab = createNavButton('认识', 'known');
        let filterTab = createNavButton('屏蔽', 'filter');
        let watchedTab = createNavButton('看过', 'watched'); // Newly added

        nav.appendChild(favoriteTab);
        nav.appendChild(knownTab);
        nav.appendChild(filterTab);
        nav.appendChild(watchedTab);

        manager.appendChild(nav);

        // Create 4 sections
        let favoriteSection = createSection('favorite', '喜爱', FAVORITES_KEY);
        let knownSection = createSection('known', '认识', KNOWN_KEY);
        let filterSection = createSection('filter', '屏蔽', STORAGE_KEY);
        let watchedSection = createSection('watched', '看过', WATCHED_KEY);

        // Initially hide all except "favorite"
        knownSection.style.display = 'none';
        filterSection.style.display = 'none';
        watchedSection.style.display = 'none';

        manager.appendChild(favoriteSection);
        manager.appendChild(knownSection);
        manager.appendChild(filterSection);
        manager.appendChild(watchedSection);

        // Settings button
        let settingsButton = document.createElement('button');
        settingsButton.id = 'settingsButton';
        settingsButton.textContent = '设置';
        settingsButton.style.display = 'none'; // Default hidden
        manager.appendChild(settingsButton);

        // Create settings modal
        let settingsModal = document.createElement('div');
        settingsModal.id = 'settingsModal';

        let settingsTitle = document.createElement('h4');
        settingsTitle.textContent = '排序设置';
        settingsModal.appendChild(settingsTitle);

        // Sort by priority
        let priorityLabel = document.createElement('label');
        let priorityCheckbox = document.createElement('input');
        priorityCheckbox.type = 'checkbox';
        priorityCheckbox.checked = getSortPriorityEnabled();
        priorityCheckbox.onchange = function () {
            GM_setValue(SORT_PRIORITY_KEY, priorityCheckbox.checked);
            modifyPage();
        };
        priorityLabel.appendChild(priorityCheckbox);
        priorityLabel.appendChild(document.createTextNode(' 按照喜爱/认识/看过排序'));
        settingsModal.appendChild(priorityLabel);

        // Sort by actress info
        let actorSortLabel = document.createElement('label');
        let actorSortCheckbox = document.createElement('input');
        actorSortCheckbox.type = 'checkbox';
        actorSortCheckbox.checked = getSortActorEnabled();
        actorSortCheckbox.onchange = function () {
            GM_setValue(SORT_ACTOR_KEY, actorSortCheckbox.checked);
            if (actorSortCheckbox.checked) {
                // Automatically load actress info
                GM_setValue(ACTORS_COLUMN_SHOULD_INSERT, true);
            }
            modifyPage();
        };
        actorSortLabel.appendChild(actorSortCheckbox);
        actorSortLabel.appendChild(document.createTextNode(' 按照演员信息排序'));
        settingsModal.appendChild(actorSortLabel);

        // Close button
        let closeButton = document.createElement('button');
        closeButton.textContent = '关闭';
        closeButton.classList.add('close-settings');
        closeButton.onclick = function () {
            settingsModal.style.display = 'none';
        };
        settingsModal.appendChild(closeButton);

        document.body.appendChild(settingsModal);

        settingsButton.onclick = function (event) {
            event.stopPropagation();
            // Calculate modal position
            const rect = settingsButton.getBoundingClientRect();
            const modalWidth = 250;
            const modalHeight = settingsModal.offsetHeight || 200;

            let top = rect.bottom + 5;
            let left = rect.left;

            if (left + modalWidth > window.innerWidth) {
                left = window.innerWidth - modalWidth - 10;
            }
            if (top + modalHeight > window.innerHeight) {
                top = rect.top - modalHeight - 5;
            }
            if (left < 10) left = 10;
            if (top < 10) top = 10;

            settingsModal.style.top = `${top}px`;
            settingsModal.style.left = `${left}px`;
            settingsModal.style.display = 'block';
        };

        // Click outside the modal to close
        window.onclick = function (event) {
            if (event.target === settingsModal) {
                settingsModal.style.display = 'none';
            }
        };

        document.body.appendChild(manager);

        // Initialize list content for each section
        updateKeywordList(FAVORITES_KEY, 'favorite');
        updateKeywordList(KNOWN_KEY, 'known');
        updateKeywordList(STORAGE_KEY, 'filter');
        updateKeywordList(WATCHED_KEY, 'watched');

        // Panel expand/collapse
        function toggleManager() {
            if (manager.classList.contains('expanded')) {
                manager.classList.remove('expanded');
                toggleIcon.textContent = '▼';
                settingsButton.style.display = 'none';
            } else {
                manager.classList.add('expanded');
                toggleIcon.textContent = '▲';
                settingsButton.style.display = 'block';
            }
        }

        function createNavButton(text, sectionId) {
            let button = document.createElement('button');
            button.textContent = text;
            button.style.flex = '1';
            button.style.padding = '5px';
            button.style.border = 'none';
            button.style.backgroundColor = '#f8f9fa';
            button.style.color = '#007bff';
            button.style.cursor = 'pointer';
            button.style.borderRadius = '4px';
            button.style.marginRight = '5px';
            button.onclick = function () { switchSection(sectionId); };
            return button;
        }

        function switchSection(section) {
            let tabs = {
                favorite: favoriteTab,
                known: knownTab,
                filter: filterTab,
                watched: watchedTab
            };
            let sections = {
                favorite: favoriteSection,
                known: knownSection,
                filter: filterSection,
                watched: watchedSection
            };
            for (let key in tabs) {
                if (key === section) {
                    tabs[key].style.backgroundColor = '#007bff';
                    tabs[key].style.color = '#fff';
                    sections[key].style.display = 'block';
                } else {
                    tabs[key].style.backgroundColor = '#f8f9fa';
                    tabs[key].style.color = '#007bff';
                    sections[key].style.display = 'none';
                }
            }
        }
    }
    /*******************************
     * Block 2
     * 6. Keyword management functions
     * 7. SQL handling functions
     * 10. Batch import and export functions
     *******************************/

    // ===== 6. Keyword management functions =====
    function getStoredKeywords(storageKey) {
        return GM_getValue(storageKey, []);
    }
    function getFilterEnabled() {
        return GM_getValue(FILTER_TOGGLE_KEY, false);
    }
    function getSortPriorityEnabled() {
        return GM_getValue(SORT_PRIORITY_KEY, false);
    }
    function getSortActorEnabled() {
        return GM_getValue(SORT_ACTOR_KEY, false);
    }

    function saveKeywords(keywords, storageKey, sectionId) {
        GM_setValue(storageKey, keywords);
        updateKeywordList(storageKey, sectionId);
        modifyPage(); // Update highlight
    }

    function addKeyword(keyword, storageKey, sectionId) {
        let keywords = getStoredKeywords(storageKey);
        if (!keywords.includes(keyword)) {
            // Ensure exclusivity across lists
            if (storageKey === FAVORITES_KEY) {
                removeKeywordByValue(keyword, KNOWN_KEY, 'known');
                removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
                removeKeywordByValue(keyword, WATCHED_KEY, 'watched');
            } else if (storageKey === KNOWN_KEY) {
                removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
                removeKeywordByValue(keyword, WATCHED_KEY, 'watched');
            } else if (storageKey === WATCHED_KEY) {
                removeKeywordByValue(keyword, FAVORITES_KEY, 'favorite');
                removeKeywordByValue(keyword, KNOWN_KEY, 'known');
                removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
            }
            keywords.push(keyword);
            saveKeywords(keywords, storageKey, sectionId);
        } else {
            alert('关键词已存在');
        }
    }

    function removeKeyword(index, storageKey, sectionId) {
        let keywords = getStoredKeywords(storageKey);
        keywords.splice(index, 1);
        saveKeywords(keywords, storageKey, sectionId);
    }

    function removeKeywordByValue(keyword, storageKey, sectionId) {
        let keywords = getStoredKeywords(storageKey);
        let idx = keywords.indexOf(keyword);
        if (idx !== -1) {
            keywords.splice(idx, 1);
            saveKeywords(keywords, storageKey, sectionId);
        }
    }

    function updateKeywordList(storageKey, sectionId) {
        let keywords = getStoredKeywords(storageKey);
        let section = document.getElementById(sectionId);
        if (!section) return;
        let list = section.querySelector('ul');
        list.innerHTML = '';

        keywords.forEach((keyword, index) => {
            let listItem = document.createElement('li');
            listItem.style.display = 'flex';
            listItem.style.justifyContent = 'space-between';
            listItem.style.alignItems = 'center';
            listItem.style.padding = '5px 0';

            let keywordText = document.createElement('span');
            keywordText.textContent = keyword;

            let buttonContainer = document.createElement('div');

            let editButton = document.createElement('button');
            editButton.textContent = '编辑';
            editButton.style.marginRight = '5px';
            editButton.style.padding = '2px 5px';
            editButton.style.backgroundColor = '#ffc107';
            editButton.style.color = '#fff';
            editButton.style.border = 'none';
            editButton.style.borderRadius = '4px';
            editButton.style.cursor = 'pointer';
            editButton.style.fontSize = '12px';
            editButton.onmouseover = function () {
                editButton.style.backgroundColor = '#e0a800';
            };
            editButton.onmouseout = function () {
                editButton.style.backgroundColor = '#ffc107';
            };
            editButton.onclick = function (event) {
                event.stopPropagation();
                showEditInput(index, keyword, storageKey, sectionId);
            };

            let deleteButton = document.createElement('button');
            deleteButton.textContent = '删除';
            deleteButton.style.padding = '2px 5px';
            deleteButton.style.backgroundColor = '#dc3545';
            deleteButton.style.color = '#fff';
            deleteButton.style.border = 'none';
            deleteButton.style.borderRadius = '4px';
            deleteButton.style.cursor = 'pointer';
            deleteButton.style.fontSize = '12px';
            deleteButton.onmouseover = function () {
                deleteButton.style.backgroundColor = '#c82333';
            };
            deleteButton.onmouseout = function () {
                deleteButton.style.backgroundColor = '#dc3545';
            };
            deleteButton.onclick = function (event) {
                event.stopPropagation();
                removeKeyword(index, storageKey, sectionId);
            };

            buttonContainer.appendChild(editButton);
            buttonContainer.appendChild(deleteButton);

            listItem.appendChild(keywordText);
            listItem.appendChild(buttonContainer);
            list.appendChild(listItem);
        });
    }

    function showEditInput(index, oldKeyword, storageKey, sectionId) {
        let newKeyword = prompt('编辑关键词', oldKeyword);
        if (newKeyword !== null && newKeyword.trim() !== '') {
            editKeyword(index, newKeyword.trim(), storageKey, sectionId);
        }
    }

    function editKeyword(index, newKeyword, storageKey, sectionId) {
        let keywords = getStoredKeywords(storageKey);
        if (!keywords.includes(newKeyword)) {
            // Ensure uniqueness
            if (storageKey === FAVORITES_KEY) {
                removeKeywordByValue(newKeyword, KNOWN_KEY, 'known');
                removeKeywordByValue(newKeyword, STORAGE_KEY, 'filter');
                removeKeywordByValue(newKeyword, WATCHED_KEY, 'watched');
            } else if (storageKey === KNOWN_KEY) {
                removeKeywordByValue(newKeyword, STORAGE_KEY, 'filter');
                removeKeywordByValue(newKeyword, WATCHED_KEY, 'watched');
            } else if (storageKey === WATCHED_KEY) {
                removeKeywordByValue(newKeyword, FAVORITES_KEY, 'favorite');
                removeKeywordByValue(newKeyword, KNOWN_KEY, 'known');
                removeKeywordByValue(newKeyword, STORAGE_KEY, 'filter');
            }
            keywords[index] = newKeyword;
            saveKeywords(keywords, storageKey, sectionId);
        } else {
            alert('关键词已存在');
        }
    }

    // ===== 7. SQL handling functions =====
    function saveSQL() {
        const editor = document.querySelector('.CodeMirror')?.CodeMirror;
        if (editor) {
            const sql = editor.getValue(); // Get SQL content from CodeMirror editor
            GM_setValue(SQL_STORAGE_KEY, sql); // Save SQL to the specified storage key
            console.log('SQL 已保存:', sql);
        } else {
            console.error('未找到 CodeMirror 编辑器');
        }
    }
    function loadSQL() {
        return GM_getValue(SQL_STORAGE_KEY, '');
    }

    // ===== 10. Batch import and export functions =====
    function showBatchImportModal(storageKey, sectionId) {
        let modal = document.createElement('div');
        modal.style.position = 'fixed';
        modal.style.top = '50%';
        modal.style.left = '50%';
        modal.style.transform = 'translate(-50%, -50%)';
        modal.style.backgroundColor = '#fff';
        modal.style.padding = '20px';
        modal.style.border = '1px solid #ccc';
        modal.style.borderRadius = '8px';
        modal.style.zIndex = '2000';
        modal.style.boxShadow = '0px 4px 10px rgba(0, 0, 0, 0.1)';

        let textarea = document.createElement('textarea');
        textarea.placeholder = '请输入要导入的关键词,使用空格或换行分隔';
        textarea.style.width = '300px';
        textarea.style.height = '150px';
        textarea.style.marginBottom = '10px';
        modal.appendChild(textarea);

        let buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex';
        buttonContainer.style.justifyContent = 'space-between';

        let importButton = document.createElement('button');
        importButton.textContent = '导入';
        importButton.style.padding = '5px 10px';
        importButton.style.backgroundColor = '#28a745';
        importButton.style.color = '#fff';
        importButton.style.border = 'none';
        importButton.style.borderRadius = '4px';
        importButton.style.cursor = 'pointer';
        importButton.onmouseover = function () {
            importButton.style.backgroundColor = '#218838';
        };
        importButton.onmouseout = function () {
            importButton.style.backgroundColor = '#28a745';
        };
        importButton.onclick = function (event) {
            event.stopPropagation();
            let inputText = textarea.value.trim();
            if (inputText) {
                let newKeywords = inputText.split(/\s+|\n+/);
                let keywords = getStoredKeywords(storageKey);
                let duplicates = [];
                newKeywords.forEach(keyword => {
                    keyword = keyword.trim();
                    if (keyword) {
                        if (!keywords.includes(keyword)) {
                            if (storageKey === FAVORITES_KEY) {
                                removeKeywordByValue(keyword, KNOWN_KEY, 'known');
                                removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
                                removeKeywordByValue(keyword, WATCHED_KEY, 'watched');
                            } else if (storageKey === KNOWN_KEY) {
                                removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
                                removeKeywordByValue(keyword, WATCHED_KEY, 'watched');
                            } else if (storageKey === WATCHED_KEY) {
                                removeKeywordByValue(keyword, FAVORITES_KEY, 'favorite');
                                removeKeywordByValue(keyword, KNOWN_KEY, 'known');
                                removeKeywordByValue(keyword, STORAGE_KEY, 'filter');
                            }
                            keywords.push(keyword);
                        } else {
                            duplicates.push(keyword);
                        }
                    }
                });
                saveKeywords(keywords, storageKey, sectionId);
                if (duplicates.length > 0) {
                    alert(`以下关键词已存在于 "${sectionId}" 列表中:\n${duplicates.join(', ')}`);
                }
            }
            document.body.removeChild(modal);
        };
        buttonContainer.appendChild(importButton);

        let cancelButton = document.createElement('button');
        cancelButton.textContent = '取消';
        cancelButton.style.padding = '5px 10px';
        cancelButton.style.backgroundColor = '#dc3545';
        cancelButton.style.color = '#fff';
        cancelButton.style.border = 'none';
        cancelButton.style.borderRadius = '4px';
        cancelButton.style.cursor = 'pointer';
        cancelButton.onmouseover = function () {
            cancelButton.style.backgroundColor = '#c82333';
        };
        cancelButton.onmouseout = function () {
            cancelButton.style.backgroundColor = '#dc3545';
        };
        cancelButton.onclick = function (event) {
            event.stopPropagation();
            document.body.removeChild(modal);
        };
        buttonContainer.appendChild(cancelButton);

        modal.appendChild(buttonContainer);
        document.body.appendChild(modal);
    }

    function exportKeywordsToCSV() {
        let favorites = getStoredKeywords(FAVORITES_KEY);
        let knowns = getStoredKeywords(KNOWN_KEY);
        let filters = getStoredKeywords(STORAGE_KEY);
        let watched = getStoredKeywords(WATCHED_KEY);

        let maxLength = Math.max(favorites.length, knowns.length, filters.length, watched.length);
        let bom = '\uFEFF';
        let csvContent = bom + '喜爱,认识,看过,屏蔽\n';

        for (let i = 0; i < maxLength; i++) {
            let fav = favorites[i] ? `"${favorites[i]}"` : '';
            let kno = knowns[i] ? `"${knowns[i]}"` : '';
            let wat = watched[i] ? `"${watched[i]}"` : '';
            let fil = filters[i] ? `"${filters[i]}"` : '';
            csvContent += `${fav},${kno},${wat},${fil}\n`;
        }

        GM_download({
            url: 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent),
            name: 'keywords.csv',
            saveAs: true
        });
    }

    /*******************************
     * Block 3
     * 8. Actress info extraction
     * 9. Actress info crawling
     * 11. Actress info insertion and hiding
     *******************************/

    // ===== 8. Actress info extraction (parse HTML) =====
    async function parseActors(doc, source, code) {
        let actors = [];
        if (source === 'javbus.com') {
            let starElements = doc.querySelectorAll('div.star-box-common li div.star-name a');
            starElements.forEach(a => {
                let actorName = a.textContent.trim();
                if (actorName) {
                    actors.push(stripParentheses(actorName));
                }
            });
            console.log(`[parseActors] javbus.com: Found actresses:`, actors);
            return actors;
        } else if (source === 'javmenu.com') {
            let actressLinks = doc.querySelectorAll('a.actress');
            actressLinks.forEach(link => {
                let actorName = link.textContent.trim();
                if (actorName) {
                    actors.push(stripParentheses(actorName));
                }
            });
            console.log(`[parseActors] javmenu.com: Found actresses:`, actors);
            return actors;
        } else if (source === 'jable.tv') {
            let modelsDiv = doc.querySelector('div.models');
            if (!modelsDiv) {
                console.warn(`[parseActors] jable.tv: Missing div.models element, returning empty array`);
                return actors;
            }
            let modelLinks = modelsDiv.querySelectorAll('a.model');
            modelLinks.forEach(link => {
                let span = link.querySelector('span.placeholder.rounded-circle');
                if (span) {
                    let actorName = span.getAttribute('data-original-title') || span.getAttribute('title');
                    if (actorName) {
                        actors.push(stripParentheses(actorName.trim()));
                    }
                }
                let img = link.querySelector('img.avatar.rounded-circle');
                if (img) {
                    let actorName = img.getAttribute('data-original-title') || img.getAttribute('title');
                    if (actorName) {
                        actors.push(stripParentheses(actorName.trim()));
                    }
                }
            });
            console.log(`[parseActors] jable.tv: Found actresses:`, actors);
            return actors;
        } else if (source === 'javdb.com') {

            // 1) Check if code is provided
            if (!code) {
                console.warn('[parseActors] javdb.com: Code not provided, cannot parse actress info');
                return actors;
            }

            // 2) Find the video detail link in search results
            let codeLink = Array.from(doc.querySelectorAll('a[href^="/v/"]')).find(link => {
                return link.textContent.trim().toUpperCase().includes(code.toUpperCase());
            });
            if (!codeLink) {
                console.warn(`[parseActors] javdb.com: No link /v/ containing ${code} found, returning empty array`);
                return actors;
            }
            let href = codeLink.getAttribute('href');
            let fullURL = `https://javdb.com${href}`;
            console.log(`[parseActors] javdb.com: Found video link => ${fullURL}`);

            // 3) Request the video detail page
            try {
                let response = await gmRequest({
                    method: 'GET',
                    url: fullURL,
                    headers: {
                        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
                        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                        'Accept-Language': 'en-US,en;q=0.5',
                        'Connection': 'keep-alive',
                        'Referer': 'https://www.google.com/',
                        'Upgrade-Insecure-Requests': '1',
                        'Cache-Control': 'max-age=0'
                    }
                });

                // 4) Check return code
                if (response.status === 200) {
                    console.log(`[parseActors] javdb.com: Got detail page successfully (status=200) => parse actress info now`);

                    // 5) Parse actress info
                    let parser = new DOMParser();
                    let detailedDoc = parser.parseFromString(response.responseText, 'text/html');
                    let panelBlocks = detailedDoc.querySelectorAll('div.panel-block');
                    // Note: "演員:" in Traditional Chinese
                    let actorDiv = Array.from(panelBlocks).find(block => {
                        let strong = block.querySelector('strong');
                        return strong && strong.textContent.trim() === '演員:';
                    });

                    if (!actorDiv) {
                        console.warn('[parseActors] javdb.com: No <strong>演員:</strong> panel-block found in detail page');
                    } else {
                        let actorLinks = actorDiv.querySelectorAll('span.value a[href^="/actors/"]');
                        actorLinks.forEach(link => {
                            let symbol = link.nextElementSibling;
                            // If male actors are needed, remove female check
                            if (symbol && symbol.classList.contains('symbol') && symbol.classList.contains('female')) {
                                let actorName = link.textContent.trim();
                                if (actorName) {
                                    actors.push(stripParentheses(actorName));
                                }
                            }
                        });
                    }

                } else {
                    console.warn(`[parseActors] javdb.com: Failed to get detail page, status=${response.status}, returning empty array`);
                }
            } catch (error) {
                console.error(`[parseActors] javdb.com: Error fetching detail page ${fullURL}:`, error);
            }

            console.log(`[parseActors] javdb.com: Final actress list =>`, actors);
            return actors;

        } else {
            // Unknown source
            return actors;
        }
    }


    // ===== 9. Actress info crawling =====
    async function crawlMissingActors(missingActors) {
        let progressOverlay = document.createElement('div');
        progressOverlay.id = 'progressOverlay';
        progressOverlay.style.position = 'fixed';
        progressOverlay.style.top = '0';
        progressOverlay.style.left = '0';
        progressOverlay.style.width = '100%';
        progressOverlay.style.height = '100%';
        progressOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
        progressOverlay.style.display = 'flex';
        progressOverlay.style.justifyContent = 'center';
        progressOverlay.style.alignItems = 'center';
        progressOverlay.style.zIndex = '10000';

        let progressBarContainer = document.createElement('div');
        progressBarContainer.id = 'progressBarContainer';
        progressBarContainer.style.width = '50%';
        progressBarContainer.style.backgroundColor = '#fff';
        progressBarContainer.style.padding = '20px';
        progressBarContainer.style.borderRadius = '8px';
        progressBarContainer.style.textAlign = 'center';

        progressBarContainer.innerHTML = `
            <h3>正在爬取演员信息...</h3>
            <div id="progressBar" style="width: 100%; background-color: #ddd; border-radius: 5px; overflow: hidden; height: 20px; margin-bottom: 10px;">
                <div style="width: 0%; height: 100%; background-color: #28a745;"></div>
            </div>
            <p id="progressText">0 / ${missingActors.length}</p>
        `;
        progressOverlay.appendChild(progressBarContainer);
        document.body.appendChild(progressOverlay);

        let progressBar = progressBarContainer.querySelector('#progressBar div');
        let progressText = progressBarContainer.querySelector('#progressText');
        let total = missingActors.length;
        let completed = 0;

        const concurrency = 4;
        let current = 0;
        let activeRequests = 0;

        async function crawl() {
            while (current < total && activeRequests < concurrency) {
                fetchActorInfo(missingActors[current]);
                current++;
            }
            if (completed >= total) {
                document.body.removeChild(progressOverlay);
                window.location.reload();
            }
        }

        async function fetchActorInfo(code) {
            activeRequests++;
            let primarySources = [
                { url: `https://www.javbus.com/${code}`, source: 'javbus.com' },
                { url: `https://javdb.com/search?q=${code}`, source: 'javdb.com' },
                { url: `https://jable.tv/videos/${code.toLowerCase()}/`, source: 'jable.tv' },
                { url: `https://javmenu.com/en/${code}`, source: 'javmenu.com' },
            ];
            let fetched = false;

            async function handleResponse(doc, source) {
                try {
                    let actors = await parseActors(doc, source, code);
                    if (actors.length > 0) {
                        let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
                        numberActorData[code] = actors;
                        GM_setValue(NUMBER_ACTOR_STORAGE_KEY, numberActorData);
                        fetched = true;
                        completeFetch();
                    } else {
                        await tryNextSource();
                    }
                } catch (error) {
                    console.error(`Error parsing actress info for code ${code}:`, error);
                    await tryNextSource();
                }
            }

            async function tryNextSource() {
                if (primarySources.length > 0) {
                    let nextSource = primarySources.shift();
                    let randomDelay = Math.floor(Math.random() * 1000) + 500;
                    await delay(randomDelay);

                    try {
                        let response = await gmRequest({
                            method: 'GET',
                            url: nextSource.url,
                            headers: {
                                'User-Agent': getRandomUserAgent(),
                                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                                'Accept-Language': 'en-US,en;q=0.5',
                                'Connection': 'keep-alive',
                                'Referer': 'https://www.google.com/',
                                'Upgrade-Insecure-Requests': '1',
                                'Cache-Control': 'max-age=0'
                            }
                        });
                        if (response.status === 200) {
                            let parser = new DOMParser();
                            let doc = parser.parseFromString(response.responseText, 'text/html');
                            await handleResponse(doc, nextSource.source);
                        } else {
                            await tryNextSource();
                        }
                    } catch (error) {
                        await tryNextSource();
                    }
                } else if (!fetched) {
                    storeEmptyActorInfo(code);
                    completeFetch();
                }
            }

            function completeFetch() {
                completed++;
                activeRequests--;
                updateProgress();
                crawl();
            }

            function storeEmptyActorInfo(code) {
                let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
                numberActorData[code] = [];
                GM_setValue(NUMBER_ACTOR_STORAGE_KEY, numberActorData);
            }

            function getRandomUserAgent() {
                const USER_AGENTS = [
                    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
                    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15',
                    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
                ];
                return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
            }

            function delay(ms) {
                return new Promise(resolve => setTimeout(resolve, ms));
            }

            await tryNextSource();
        }

        function updateProgress() {
            progressBar.style.width = `${(completed / total) * 100}%`;
            progressText.textContent = `${completed} / ${total}`;
        }

        crawl();
    }

    // ===== 11. Actress info insertion and hiding =====
    function toggleActorsColumn() {
        let shouldInsertActor = GM_getValue(ACTORS_COLUMN_SHOULD_INSERT, false);
        if (!shouldInsertActor) {
            if (!GM_getValue(ACTORS_COLUMN_INSERTED_KEY)) {
                insertActorsInfo();
                GM_setValue(ACTORS_COLUMN_INSERTED_KEY, true);
            }
            GM_setValue(ACTORS_COLUMN_SHOULD_INSERT, true);
        } else {
            if (GM_getValue(ACTORS_COLUMN_INSERTED_KEY)) {
                hideActorsInfo();
                GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
            }
            GM_setValue(ACTORS_COLUMN_SHOULD_INSERT, false);
        }
        updateLoadActorsButton();
    }

    function updateLoadActorsButton() {
        let loadActorsButton = document.getElementById('loadActorsButton');
        if (!loadActorsButton) return;
        let shouldInsert = GM_getValue(ACTORS_COLUMN_SHOULD_INSERT);
        loadActorsButton.textContent = shouldInsert ? '隐藏演员信息' : '加载演员信息';
    }

    function hideActorsInfo() {
        if (isJinjierArt()) {
            hideActorsColumnJinjierArt();
        } else if (isJavdbRankings()) {
            hideActorsInfoJavdb();
        } else if (isJavLibrary()) {
            hideActorsInfoJavLibrary();
        }
    }

    function insertActorsInfo() {
        if (isJinjierArt()) {
            loadActorsAndInsertColumnJinjierArt();
        } else if (isJavdbRankings()) {
            loadActorsAndInsertInfoJavdb();
        } else if (isJavLibrary()) {
            loadActorsAndInsertInfoJavLibrary();
        }
        GM_setValue(ACTORS_COLUMN_INSERTED_KEY, true);
    }

    function isJinjierArt() {
        return window.location.hostname.includes('jinjier.art');
    }

    function isJavdbRankings() {
        return window.location.hostname.includes('javdb.com') && !window.location.pathname.startsWith('/actors/') && !window.location.pathname.startsWith('/v/');
    }

    function isJavLibrary() {
        // Simple check if domain contains "javlibrary.com"
        return window.location.hostname.includes('javlibrary.com');
    }

    function hideActorsColumnJinjierArt() {
        let table = document.querySelector('table');
        if (!table) return;
        let rows = table.querySelectorAll('tr');
        rows.forEach(row => {
            if (row.cells.length > 0) {
                row.deleteCell(0);
            }
        });
    }

    function hideActorsInfoJavdb() {
        let items = document.querySelectorAll('div.item');
        items.forEach(item => {
            let actorDiv = item.querySelector('.actors-info');
            if (actorDiv) actorDiv.remove();
        });
    }

    function hideActorsInfoJavLibrary() {
        let videos = document.querySelectorAll('div.video');
        videos.forEach(video => {
            let actorDiv = video.querySelector('.actors-info');
            if (actorDiv) {
                actorDiv.remove();
            }
        });
    }

    function loadActorsAndInsertColumnJinjierArt() {
        let codes = new Set();
        let links = document.querySelectorAll('a');
        links.forEach(link => {
            let text = link.textContent.trim();
            let codeMatch = text.match(/[A-Z]{2,5}-\d{2,5}/i);
            if (codeMatch) {
                codes.add(codeMatch[0].toUpperCase());
            }
        });

        let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
        let missingActors = [];
        codes.forEach(code => {
            if (!numberActorData[code] || !Array.isArray(numberActorData[code])) {
                missingActors.push(code);
            }
        });
        if (missingActors.length > 0) {
            let proceed = confirm(`发现 ${missingActors.length} 个番号没有对应的演员信息。是否开始爬取?`);
            if (proceed) {
                crawlMissingActors(missingActors);
            }
        }

        let codeActorMap = {};
        codes.forEach(code => {
            if (numberActorData[code] && Array.isArray(numberActorData[code])) {
                codeActorMap[code] = numberActorData[code];
            }
        });

        let table = document.querySelector('table');
        if (!table) return;
        let rows = table.querySelectorAll('tr');
        rows.forEach((row) => {
            let codeCell = row.querySelector('td a');
            let actorsHTML = '';
            if (codeCell) {
                let codeText = codeCell.textContent.trim().toUpperCase();
                let actors = codeActorMap[codeText] || [];
                actorsHTML = actors.map(actor => `<div><a href="https://javdb.com/search?f=actor&locale=zh&q=${encodeURIComponent(actor)}" target="_blank">${stripParentheses(actor)}</a></div>`).join('');
            }
            let newCell = row.insertCell(0);
            newCell.innerHTML = actorsHTML;
        });
    }

    function loadActorsAndInsertInfoJavdb() {
        let codes = new Set();
        let items = document.querySelectorAll('div.item');
        items.forEach(item => {
            let videoTitle = item.querySelector('.video-title');
            if (videoTitle) {
                let strong = videoTitle.querySelector('strong');
                if (strong) {
                    let code = strong.textContent.trim().toUpperCase();
                    let codeMatch = code.match(/[A-Z]{2,5}-\d{2,5}/i);
                    if (codeMatch) {
                        codes.add(codeMatch[0].toUpperCase());
                    }
                }
            }
        });

        let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
        let missingActors = [];
        codes.forEach(code => {
            if (!numberActorData[code] || !Array.isArray(numberActorData[code])) {
                missingActors.push(code);
            }
        });
        if (missingActors.length > 0) {
            let proceed = confirm(`发现 ${missingActors.length} 个番号没有对应的演员信息。是否开始爬取?`);
            if (proceed) {
                crawlMissingActors(missingActors);
            }
        }

        let codeActorMap = {};
        codes.forEach(code => {
            if (numberActorData[code] && Array.isArray(numberActorData[code])) {
                codeActorMap[code] = numberActorData[code];
            }
        });

        let insertedItems = 0;
        items.forEach(item => {
            let videoTitle = item.querySelector('.video-title');
            if (videoTitle) {
                let strong = videoTitle.querySelector('strong');
                if (strong) {
                    let codeText = strong.textContent.trim().toUpperCase();
                    let actors = codeActorMap[codeText] || [];
                    let actorsHTML = actors.map(actor => `<div><a href="https://javdb.com/search?f=actor&locale=zh&q=${encodeURIComponent(actor)}" target="_blank">${stripParentheses(actor)}</a></div>`).join('');
                    let actorDiv = document.createElement('div');
                    actorDiv.classList.add('actors-info');
                    actorDiv.innerHTML = `<strong>演员:</strong> ${actorsHTML}`;
                    actorDiv.style.marginBottom = '5px';
                    videoTitle.parentNode.insertBefore(actorDiv, videoTitle);
                    insertedItems++;
                }
            }
        });
    }

    function loadActorsAndInsertInfoJavLibrary() {
        // 1) Collect all codes
        let codes = new Set();
        let videoDivs = document.querySelectorAll('.video');

        videoDivs.forEach(video => {
            let codeElem = video.querySelector('.id');
            if (codeElem) {
                let codeText = codeElem.textContent.trim().toUpperCase();
                let codeMatch = codeText.match(/[A-Z]{2,5}-\d{2,5}/i);
                if (codeMatch) {
                    codes.add(codeMatch[0].toUpperCase());
                }
            }
        });

        // 2) Check existing number-actor mapping, see if any missing
        let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});
        let missingActors = [];
        codes.forEach(code => {
            if (!numberActorData[code] || !Array.isArray(numberActorData[code])) {
                missingActors.push(code);
            }
        });
        if (missingActors.length > 0) {
            let proceed = confirm(`发现 ${missingActors.length} 个番号没有对应的演员信息。是否开始爬取?`);
            if (proceed) {
                crawlMissingActors(missingActors);
            }
        }

        // 3) Construct code => [actors] map
        let codeActorMap = {};
        codes.forEach(code => {
            if (numberActorData[code] && Array.isArray(numberActorData[code])) {
                codeActorMap[code] = numberActorData[code];
            }
        });

        // 4) Insert actress info into each videoDiv
        let insertedItems = 0;
        videoDivs.forEach(video => {
            let codeElem = video.querySelector('.id');
            if (!codeElem) return;

            let codeText = codeElem.textContent.trim().toUpperCase();
            let codeMatch = codeText.match(/[A-Z]{2,5}-\d{2,5}/i);
            if (!codeMatch) return;

            let finalCode = codeMatch[0].toUpperCase();
            let actors = codeActorMap[finalCode] || [];

            if (actors.length > 0) {
                let actorsHTML = actors.map(actor =>
                    `<div><a href="https://javdb.com/search?f=actor&locale=zh&q=${encodeURIComponent(actor)}" target="_blank">
                        ${stripParentheses(actor)}
                     </a></div>`
                ).join('');

                let actorDiv = document.createElement('div');
                actorDiv.classList.add('actors-info');
                actorDiv.innerHTML = `<strong>演员:</strong> ${actorsHTML}`;
                actorDiv.style.marginBottom = '5px';

                let titleElem = video.querySelector('.title');
                if (titleElem) {
                    titleElem.insertAdjacentElement('afterend', actorDiv);
                    insertedItems++;
                }
            }
        });

        console.log('[loadActorsAndInsertInfoJavLibrary] Insert actress info successfully:', insertedItems, 'items');
    }

    /*******************************
     * Block 4
     * 12. Highlight and sorting
     * 13. Floating buttons
     * 14. modifyPage function
     * 15. init function
     *******************************/

    // ===== 12. Highlight and sorting =====
    function modifyPage() {
        if (isSelectingText) return;
        try {
            let shouldInsertActors = GM_getValue(ACTORS_COLUMN_SHOULD_INSERT, false);
            let ActorsInserted = GM_getValue(ACTORS_COLUMN_INSERTED_KEY, false);
            if (shouldInsertActors && !ActorsInserted) {
                insertActorsInfo();
            }
            updateLoadActorsButton();

            let favorites = getStoredKeywords(FAVORITES_KEY);
            let knowns = getStoredKeywords(KNOWN_KEY);
            let watched = getStoredKeywords(WATCHED_KEY);
            let filters = getStoredKeywords(STORAGE_KEY);
            let sortPriorityEnabled = getSortPriorityEnabled();
            let sortActorEnabled = getSortActorEnabled();

            if (isJinjierArt()) {
                highlightRowsJinjierArt(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled);
            } else if (isJavdbRankings()) {
                highlightRowsJavdb(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled);
            } else if (isJavLibrary()) {
                highlightRowsJavLibrary(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled);
            }
        } catch (error) {
            console.error('Error in modifyPage:', error);
        }
    }

    function highlightRowsJinjierArt(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled) {
        let rows = Array.from(document.querySelectorAll('table tr'));
        let rowDataArray = [];

        rows.forEach((row, rowIndex) => {
            let cells = row.querySelectorAll('td');
            let isFavorite = false;
            let isKnown = false;
            let isWatched = false;
            let actors = [];

            cells.forEach((cell, cellIndex) => {
                if (!cell.hasAttribute('data-original-html')) {
                    cell.setAttribute('data-original-html', cell.innerHTML);
                } else {
                    cell.innerHTML = cell.getAttribute('data-original-html');
                }

                let cellText = cell.textContent;
                if (favorites.some(k => cellText.includes(k))) {
                    isFavorite = true;
                }
                if (knowns.some(k => cellText.includes(k))) {
                    isKnown = true;
                }
                if (watched.some(k => cellText.includes(k))) {
                    isWatched = true;
                }

                if (cellIndex === 0) {
                    let actorDivs = cell.querySelectorAll('div');
                    actorDivs.forEach(div => {
                        let actorName = div.textContent.trim();
                        if (actorName) actors.push(actorName);
                    });
                }
            });

            if (isWatched) {
                row.style.backgroundColor = 'lightblue';
            } else if (isFavorite) {
                row.style.backgroundColor = 'lightgreen';
            } else if (isKnown) {
                row.style.backgroundColor = 'yellow';
            } else {
                row.style.backgroundColor = '';
            }

            let shouldHide = false;
            cells.forEach(cell => {
                let cellText = cell.textContent;
                if (filters.some(k => cellText.includes(k))) {
                    shouldHide = true;
                }
            });
            shouldHide = shouldHide && getFilterEnabled();
            row.style.display = shouldHide ? 'none' : '';

            rowDataArray.push({
                rowElement: row,
                isWatched, isFavorite, isKnown,
                actors,
                originalIndex: rowIndex
            });
        });

        if (sortPriorityEnabled || sortActorEnabled) {
            rowDataArray.sort((a, b) => {
                if (sortPriorityEnabled) {
                    if (a.isWatched !== b.isWatched) return a.isWatched ? -1 : 1;
                    if (a.isFavorite !== b.isFavorite) return a.isFavorite ? -1 : 1;
                    if (a.isKnown !== b.isKnown) return a.isKnown ? -1 : 1;
                }
                if (sortActorEnabled) {
                    let aActors = a.actors.join(', ') || '\uFFFF';
                    let bActors = b.actors.join(', ') || '\uFFFF';
                    if (aActors < bActors) return -1;
                    if (aActors > bActors) return 1;
                }
                return a.originalIndex - b.originalIndex;
            });
        }

        let tbody = document.querySelector('table tbody');
        if (tbody) {
            tbody.innerHTML = '';
            rowDataArray.forEach(rowData => {
                tbody.appendChild(rowData.rowElement);
            });
        }
    }

    function highlightRowsJavdb(favorites, knowns, watched, filters, sortPriorityEnabled, sortActorEnabled) {
        let items = Array.from(document.querySelectorAll('div.movie-list.h.cols-4.vcols-8 > div.item'));
        let itemDataArray = [];

        items.forEach((item, itemIndex) => {
            let videoTitleElem = item.querySelector('.video-title');
            let isFavorite = false;
            let isKnown = false;
            let isWatched = false;
            let actors = [];

            if (videoTitleElem) {
                let textContent = videoTitleElem.textContent;
                if (favorites.some(k => textContent.includes(k))) {
                    isFavorite = true;
                }
                if (knowns.some(k => textContent.includes(k))) {
                    isKnown = true;
                }
                if (watched.some(k => textContent.includes(k))) {
                    isWatched = true;
                }

                let actorDiv = item.querySelector('.actors-info');
                if (actorDiv) {
                    let actorLinks = actorDiv.querySelectorAll('a');
                    actorLinks.forEach(link => {
                        let actorName = link.textContent.trim();
                        if (actorName) actors.push(actorName);
                    });
                }
            }

            let box = item.querySelector('a.box');
            if (box) {
                if (isWatched) {
                    box.style.backgroundColor = 'lightblue';
                } else if (isFavorite) {
                    box.style.backgroundColor = 'lightgreen';
                } else if (isKnown) {
                    box.style.backgroundColor = 'yellow';
                } else {
                    box.style.backgroundColor = '';
                }
            }

            let shouldHide = false;
            if (box) {
                let boxText = box.textContent;
                if (filters.some(k => boxText.includes(k))) {
                    shouldHide = true;
                }
            }
            shouldHide = shouldHide && getFilterEnabled();
            item.style.display = shouldHide ? 'none' : '';

            itemDataArray.push({
                itemElement: item,
                isWatched,
                isFavorite,
                isKnown,
                actors,
                originalIndex: itemIndex
            });
        });

        if (sortPriorityEnabled || sortActorEnabled) {
            itemDataArray.sort((a, b) => {
                if (sortPriorityEnabled) {
                    if (a.isWatched !== b.isWatched) return a.isWatched ? -1 : 1;
                    if (a.isFavorite !== b.isFavorite) return a.isFavorite ? -1 : 1;
                    if (a.isKnown !== b.isKnown) return a.isKnown ? -1 : 1;
                }
                if (sortActorEnabled) {
                    let aActors = a.actors.join(', ') || '\uFFFF';
                    let bActors = b.actors.join(', ') || '\uFFFF';
                    if (aActors < bActors) return -1;
                    if (aActors > bActors) return 1;
                }
                return a.originalIndex - b.originalIndex;
            });
        }

        let container = document.querySelector('div.movie-list.h.cols-4.vcols-8');
        if (container) {
            container.innerHTML = '';
            itemDataArray.forEach(itemData => {
                container.appendChild(itemData.itemElement);
            });
        }
    }

    function highlightRowsJavLibrary(
        favorites,
        knowns,
        watched,
        filters,
        sortPriorityEnabled,
        sortActorEnabled
    ) {
        // 0) Retrieve stored "code -> [actress array]" mapping
        let numberActorData = GM_getValue(NUMBER_ACTOR_STORAGE_KEY, {});

        // 1) Find the container that holds all video items (e.g. <div class="videos">)
        let container = document.querySelector('.videos');
        if (!container) {
            console.warn('[highlightRowsJavLibrary] .videos container not found, exiting');
            return;
        }

        // 2) Get all video elements, each is <div class="video" id="vid_XXXXX">
        let items = Array.from(container.querySelectorAll('div.video'));

        // 3) Collect item info for potential sorting
        let itemDataArray = [];

        items.forEach((item, index) => {
            // 3.1) Get code and title
            let codeElem = item.querySelector('.id');
            let titleElem = item.querySelector('.title');

            let codeText = codeElem ? codeElem.textContent.trim().toUpperCase() : '';
            let titleText = titleElem ? titleElem.textContent.trim() : '';

            // 3.2) Based on code, retrieve actress array from numberActorData
            let codeMatch = codeText.match(/[A-Z]{2,5}-\d{2,5}/i);
            let codeKey = codeMatch ? codeMatch[0].toUpperCase() : null;
            let itemActors = (codeKey && Array.isArray(numberActorData[codeKey]))
                ? numberActorData[codeKey]
                : [];

            // 3.3) Determine if this video matches favorites/known/watched by code/title
            let isFavorite = false;
            let isKnown = false;
            let isWatched = false;
            let shouldHide = false;

            if (favorites.some(k => codeText.includes(k) || titleText.includes(k))) {
                isFavorite = true;
            }
            if (knowns.some(k => codeText.includes(k) || titleText.includes(k))) {
                isKnown = true;
            }
            if (watched.some(k => codeText.includes(k) || titleText.includes(k))) {
                isWatched = true;
            }
            if (filters.some(k => codeText.includes(k) || titleText.includes(k))) {
                shouldHide = true;
            }

            // 3.4) Check actress info for matches
            let actorHasFavorite = itemActors.some(actor =>
                favorites.some(k => actor.includes(k))
            );
            let actorHasKnown = itemActors.some(actor =>
                knowns.some(k => actor.includes(k))
            );
            let actorHasWatched = itemActors.some(actor =>
                watched.some(k => actor.includes(k))
            );
            let actorHasFilter = itemActors.some(actor =>
                filters.some(k => actor.includes(k))
            );

            if (actorHasFavorite) isFavorite = true;
            if (actorHasKnown) isKnown = true;
            if (actorHasWatched) isWatched = true;

            if (actorHasFilter) shouldHide = true;
            // If the first three are not matched but there's a filter, set true
            if (!isFavorite && !isKnown && !isWatched && shouldHide) {
                shouldHide = true;
            } else {
                shouldHide = false;
            }

            // 3.5) Assign background color: watched > favorite > known
            if (isWatched) {
                item.style.backgroundColor = 'lightblue';
            } else if (isFavorite) {
                item.style.backgroundColor = 'lightgreen';
            } else if (isKnown) {
                item.style.backgroundColor = 'yellow';
            } else {
                item.style.backgroundColor = '';
            }

            // Enable hide only if filter toggle is ON
            shouldHide = shouldHide && getFilterEnabled();
            item.style.display = shouldHide ? 'none' : '';

            // 3.6) Save info into array
            itemDataArray.push({
                itemElement: item,
                isWatched,
                isFavorite,
                isKnown,
                actors: itemActors,
                originalIndex: index
            });
        });

        // 4) If we enable priority or actress sorting
        if (sortPriorityEnabled || sortActorEnabled) {
            itemDataArray.sort((a, b) => {
                // 4.1) Priority sorting: watched > favorite > known
                if (sortPriorityEnabled) {
                    if (a.isWatched !== b.isWatched) return a.isWatched ? -1 : 1;
                    if (a.isFavorite !== b.isFavorite) return a.isFavorite ? -1 : 1;
                    if (a.isKnown !== b.isKnown) return a.isKnown ? -1 : 1;
                }
                // 4.2) Actress sorting
                if (sortActorEnabled) {
                    let aActors = a.actors.join(', ') || '\uFFFF';
                    let bActors = b.actors.join(', ') || '\uFFFF';
                    if (aActors < bActors) return -1;
                    if (aActors > bActors) return 1;
                }
                // 4.3) If none of the above, follow original DOM order
                return a.originalIndex - b.originalIndex;
            });
        }

        // 5) After sorting, re-append itemElements to container
        container.innerHTML = '';
        itemDataArray.forEach(itemData => {
            container.appendChild(itemData.itemElement);
        });
    }

    // ===== 13. Floating buttons =====
    function createFloatingButtons() {
        let filterButton = document.createElement('button');
        filterButton.id = 'filterButton';
        filterButton.textContent = '屏蔽';
        filterButton.classList.add('floating-button');
        document.body.appendChild(filterButton);

        let favoriteButton = document.createElement('button');
        favoriteButton.id = 'favoriteButton';
        favoriteButton.textContent = '喜爱';
        favoriteButton.classList.add('floating-button');
        document.body.appendChild(favoriteButton);

        let knownButton = document.createElement('button');
        knownButton.id = 'knownButton';
        knownButton.textContent = '认识';
        knownButton.classList.add('floating-button');
        document.body.appendChild(knownButton);

        let watchedButton = document.createElement('button');
        watchedButton.id = 'watchedButton';
        watchedButton.textContent = '看过';
        watchedButton.classList.add('floating-button');
        document.body.appendChild(watchedButton);

        document.addEventListener('mousedown', function () {
            isSelectingText = true;
        });
        document.addEventListener('mouseup', function (event) {
            isSelectingText = false;
            setTimeout(function () {
                let selectedText = window.getSelection().toString().trim();
                if (selectedText.length > 0) {
                    let mouseX = event.clientX;
                    let mouseY = event.clientY;
                    let offset = 20;

                    filterButton.style.left = mouseX + 'px';
                    filterButton.style.top = (mouseY + offset) + 'px';
                    filterButton.style.display = 'block';

                    favoriteButton.style.left = (mouseX + 60) + 'px';
                    favoriteButton.style.top = (mouseY + offset) + 'px';
                    favoriteButton.style.display = 'block';

                    knownButton.style.left = (mouseX + 120) + 'px';
                    knownButton.style.top = (mouseY + offset) + 'px';
                    knownButton.style.display = 'block';

                    watchedButton.style.left = (mouseX + 180) + 'px';
                    watchedButton.style.top = (mouseY + offset) + 'px';
                    watchedButton.style.display = 'block';

                    filterButton.onclick = function (e) {
                        e.stopPropagation();
                        addKeyword(selectedText, STORAGE_KEY, 'filter');
                        hideFloatingButtons();
                    };
                    favoriteButton.onclick = function (e) {
                        e.stopPropagation();
                        addKeyword(selectedText, FAVORITES_KEY, 'favorite');
                        hideFloatingButtons();
                    };
                    knownButton.onclick = function (e) {
                        e.stopPropagation();
                        addKeyword(selectedText, KNOWN_KEY, 'known');
                        hideFloatingButtons();
                    };
                    watchedButton.onclick = function (e) {
                        e.stopPropagation();
                        addKeyword(selectedText, WATCHED_KEY, 'watched');
                        hideFloatingButtons();
                    };
                } else {
                    hideFloatingButtons();
                }
            }, 0);
        });

        function hideFloatingButtons() {
            filterButton.style.display = 'none';
            favoriteButton.style.display = 'none';
            knownButton.style.display = 'none';
            watchedButton.style.display = 'none';
        }
    }
    createFloatingButtons();

    // ===== 14. modifyPage function =====
    function setupExecuteButtonListener() {
        let executeButton = document.getElementById('execute');
        if (executeButton) {
            executeButton.addEventListener('click', function (event) {
                event.stopPropagation();
                GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
                saveSQL();
                modifyPage();
            });
        }
    }
    function setupKeyboardShortcuts() {
        document.addEventListener('keydown', function (event) {
            if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
                event.preventDefault();
                GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
                saveSQL();
                modifyPage();
            }
        });
    }

    // ===== 15. init function =====
    function initJinjier() {
        console.log('Initializing jinjier.art...');
        GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
        setupExecuteButtonListener();
        setupKeyboardShortcuts();
        setTimeout(() => {
            let editor = document.querySelector('.CodeMirror')?.CodeMirror;
            if (editor) {
                let lastSQL = loadSQL();
                if (lastSQL) {
                    editor.setValue(lastSQL);
                }
                let executeButton = document.getElementById('execute');
                if (executeButton) {
                    // Automatically click once to execute SQL
                    executeButton.click();
                }
            }
        }, 1000);
    }

    function initJavdb() {
        console.log('Initializing javdb.com/rankings...');
        GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
        setTimeout(() => {
            modifyPage();
        }, 500);
    }

    function initJavLibrary() {
        console.log('Initializing javlibrary.com...');

        const style = document.createElement('style');
        style.innerHTML = `
      .video {
        height: auto !important;
        max-height: 380px !important;
        overflow-y: auto !important;
      }
    `;
        document.head.appendChild(style);
        GM_setValue(ACTORS_COLUMN_INSERTED_KEY, false);
        setTimeout(() => {
            modifyPage();
        }, 500);
    }

    function init() {
        createKeywordManager();
        const hostname = window.location.hostname;
        const pathname = window.location.pathname;
        if (isJinjierArt()) {
            initJinjier();
        } else if (isJavdbRankings()) {
            initJavdb();
        } else if (isJavLibrary()) {
            initJavLibrary();
        } else {
            console.log('Detected other site, applying minimal logic...');
        }
    }
    init();
})();