TradingView Theme Customizer

Customize TradingView theme colors

目前為 2024-01-31 提交的版本,檢視 最新版本

// ==UserScript==
// @name         TradingView Theme Customizer
// @namespace    https://gf.qytechs.cn/en/users/742563-666-999
// @version      1.0
// @description  Customize TradingView theme colors
// @author       666 999
// @match        https://www.tradingview.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const COMMON_STYLES = `
    padding: 0;
    border: none;
    margin-top: none;
    width: 18px;
    height: 18px;
  `;

    GM_addStyle(`
    input[type="color"]::-webkit-color-swatch-wrapper {
      ${COMMON_STYLES}
    }

    input[type="color"]::-webkit-color-swatch,
    input[type="color"]::-moz-color-swatch {
      ${COMMON_STYLES}
    }

  /* Animation for menu extension */
  #themeMenu {
    transition: width 0.1s linear;
  }
  `);

    const MENU_WIDTH_EXTENDED = '574px';
    const MENU_WIDTH_COLLAPSED = '220px';
    const BUY_COFFEE_WIDTH = '424px';

    const COLOR_GROUPS = {
        color1: [
            '--tv-color-pane-background',
            '--tv-color-toolbar-divider-background',
            '--tv-color-popup-element-divider-background',
        ],
        color2: [
            '--tv-color-toolbar-save-layout-loader',
            '--tv-color-popup-background',
            '--tv-color-popup-element-toolbox-background-hover',
        ],
        color3: ['--tv-color-platform-background'],
        color4: [
            '--tv-color-toolbar-button-text',
            '--tv-color-popup-element-text',
            '--tv-color-popup-element-toolbox-text',
        ],
        color5: [
            '--tv-color-toolbar-button-text-hover',
            '--tv-color-popup-element-text-hover',
            '--tv-color-popup-element-secondary-text',
            '--tv-color-popup-element-hint-text',
            '--tv-color-popup-element-toolbox-text-hover',
            '--tv-color-popup-element-toolbox-text-active-hover',
        ],
        color6: [
            '--tv-color-toolbar-button-background-hover',
            '--tv-color-toolbar-button-background-expanded',
            '--tv-color-toolbar-button-background-active-hover',
            '--tv-color-popup-element-background-hover',
        ],
        color7: [
            '--tv-color-item-active-text',
            '--tv-color-popup-element-text-active',
            '--tv-color-toolbar-button-text-clicked', // new
        ],
        color8: [
            '--tv-color-toolbar-button-text-active',
            '--tv-color-toolbar-button-text-active-hover',
            '--tv-color-toolbar-toggle-button-background-active',
            '--tv-color-popup-element-background-active',
        ],
        color9: [
            '--tv-color-toolbar-toggle-button-background-active-hover',
            '--tv-color-popup-element-toolbox-background-active-hover',
        ],
        color10: ['--tv-color-toolbar-button-background-active'],
        //new ones, WIP
        /*color11: [
                 //'--tv-color-toolbar-button-background',
                 //'--tv-color-toolbar-button-text-clicked',
                 //'--tv-color-toolbar-button-background-clicked'
        ],*/
    };

    const TITLES = {
        color1: 'Pane',
        color2: 'Favorites Toolbar & Menu',
        color3: 'Background',
        color4: 'Buttons',
        color5: 'Button Hover',
        color6: 'Button Hover Background',
        color7: 'Active Item Name / Button Click',
        color8: 'Active Button / Item',
        color9: 'Active Bottom Button Hover',
        color10: 'Active Button Right Panel',
    };

    const DEFAULT_COLORS = {
        color1: '#2e2f37', color2: '#321a24', color3: '#1b1c24', color4: '#da8bb1', color5: '#ff0000',
        color6: '#f48fb1', color7: '#000000', color8: '#cc285f', color9: '#4dd0e1', color10: '#1b1c24',
    };

    // tradingview's pink theme
    const PRESET_COLORS_2 = {
        color1: '#fbdff4', color2: '#868993', color3: '#d1c4e9', color4: '#88184f', color5: '#4a148c',
        color6: '#f48fb1', color7: '#0606ff', color8: '#ff0000', color9: '#ff00ff', color10: '#f9b9e9',
    };

    const PRESET_COLORS_3 = {
        color1: '#000000', color2: '#1c1c1c', color3: '#003338', color4: '#17bee8', color5: '#24f07c',
        color6: '#000000', color7: '#000000', color8: '#ff8838', color9: '#941600', color10: '#000000',
    };

    // Initialize savedColors with stored settings or DEFAULT_COLORS
    const savedColors = GM_getValue('customColors') || { ...DEFAULT_COLORS };

    // Helper Function: Copy Text to Clipboard
    function copyToClipboard(text) {
        const textarea = document.createElement('textarea');
        textarea.value = text;
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand('copy');
        document.body.removeChild(textarea);
    }

    // Function to display a right-top speech balloon
    function displayBalloon(menu, message) {
        // Remove existing balloon if present
        const existingBalloon = document.getElementById('balloon');
        if (existingBalloon) {
            menu.removeChild(existingBalloon);
        }

        // Create a balloon container
        const balloon = document.createElement('div');
        balloon.id = 'balloon';
        balloon.style.position = 'absolute';
        balloon.style.top = '74px';
        balloon.style.right = '30%';
        balloon.style.zIndex = '1002';

        // Create the balloon content
        const content = document.createElement('div');
        content.textContent = message;
        content.style.background = 'darkgrey';
        content.style.color = '#222';
        content.style.padding = '10px';
        content.style.borderRadius = '5px';

        // Append the content to the balloon container
        balloon.appendChild(content);

        // Create a tail element using ::before pseudo-element
        const tail = document.createElement('div');
        tail.id = 'tail';
        tail.style.position = 'absolute';
        tail.style.borderStyle = 'solid';
        tail.style.borderWidth = '10px';
        tail.style.borderColor = 'transparent transparent transparent darkgrey';
        tail.style.top = '23.5%';
        tail.style.left = '100%';

        balloon.appendChild(tail);

        // Append the balloon to the menu container
        menu.appendChild(balloon);

        // Hide the balloon after a short delay (e.g., 1.5 seconds)
        setTimeout(() => {
            menu.removeChild(balloon);
        }, 1500);
    }

    // Add a variable to store the menu position
    let menuPosition = { top: '95px', left: '56px' };
    // Helper Function: Make Menu Movable
    function makeMovable(menu, applyThemeMenuStyles) {
        let isDragging = false;
        let offsetX, offsetY;

        menu.addEventListener('mousedown', (event) => {
            isDragging = true;
            offsetX = event.clientX - menu.getBoundingClientRect().left;
            offsetY = event.clientY - menu.getBoundingClientRect().top;
        });

        document.addEventListener('mousemove', (event) => {
            if (isDragging) {
                menu.style.left = event.clientX - offsetX + 'px';
                menu.style.top = event.clientY - offsetY + 'px';
            }
        });

        document.addEventListener('mouseup', () => {
            if (isDragging) {
                // Update the menu position when dragging stops
                menuPosition = { top: menu.style.top, left: menu.style.left };
            }
            isDragging = false;
        });
    }

    // Helper Function: Apply Colors to Theme
    function applyColors() {
        Object.entries(COLOR_GROUPS).forEach(([group, properties]) => {
            properties.forEach((property) => {
                document.documentElement.style.setProperty(property, savedColors[group] || DEFAULT_COLORS[group]);
            });
        });
    }

    // Helper Function: Apply Theme Menu Styles
    function applyThemeMenuStyles(menu, isExtended) {
        const menuWidth = isExtended ? MENU_WIDTH_EXTENDED : MENU_WIDTH_COLLAPSED;
        menu.style.width = menuWidth;
        menu.style.overflow = 'hidden';
        menu.style.position = 'fixed';
        // Update menu position directly
        menu.style.top = menuPosition.top;
        menu.style.left = menuPosition.left;
        menu.style.padding = '10px';
        menu.style.color = savedColors.color4 || DEFAULT_COLORS.color4;
        menu.style.background = savedColors.color2 || DEFAULT_COLORS.color2;
        menu.style.border = `1px solid ${savedColors.color8 || DEFAULT_COLORS.color8}`;
        menu.style.zIndex = '1000';
        menu.style.display = 'flex';
        menu.style.flexDirection = 'column';
        menu.style.borderRadius = '5px';

        // Make the menu movable
        makeMovable(menu);

        // "Buy me a coffee" section
        const hiddenPart = document.createElement('div');
        hiddenPart.style.width = BUY_COFFEE_WIDTH;
        hiddenPart.style.overflow = 'hidden';
        hiddenPart.style.position = 'absolute';
        hiddenPart.style.top = '0';
        hiddenPart.style.right = `-${BUY_COFFEE_WIDTH}`;
        hiddenPart.style.left = '220px';
        hiddenPart.style.background = savedColors.color2 || DEFAULT_COLORS.color2;
        hiddenPart.style.color = savedColors.color4 || DEFAULT_COLORS.color4;

        const buyCoffeeText = document.createElement('p');
        buyCoffeeText.style.fontFamily = 'Consolas, monospace';
        buyCoffeeText.style.fontSize = '14px';
        buyCoffeeText.innerHTML = '<span style="font-size: 12px;">Consider support or buying me coffee</span>\
        <br><br>\
        /\\_____/\\<br>\
        /&nbsp;&nbsp;o&nbsp;&nbsp;&nbsp;o&nbsp;&nbsp;\\<br>\
        (&nbsp;==&nbsp;&nbsp;^&nbsp;&nbsp;==&nbsp;)<br>\
        )&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(<br>\
        (&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)<br>\
        (&nbsp;(&nbsp;&nbsp;)&nbsp;&nbsp;&nbsp;(&nbsp;&nbsp;)&nbsp;)<br>\
        (__(__)___(__)__)<br>\
        ';
        buyCoffeeText.style.padding = '7px';
        buyCoffeeText.style.textAlign = 'center';

        const trc20Text = document.createElement('span');
        trc20Text.style.fontSize = '11px';
        trc20Text.innerHTML = 'USDT TRC20: ';

        const trc20Address = document.createElement('span');
        trc20Address.textContent = 'TSvizdjmrtXjYEEcUDzbvwLSLEbAJYXARA';
        trc20Address.style.cursor = 'pointer';
        trc20Address.addEventListener('click', function () {
            copyToClipboard(trc20Address.textContent);
            displayBalloon(menu, 'TRC-20 address copied to clipboard');
        });

        const ethText = document.createElement('span');
        ethText.style.fontSize = '11px';
        ethText.innerHTML = '<br>USDT on Polygon(ERC20) & BSC(BEP20):<br>';

        const ethAddress = document.createElement('span');
        ethAddress.textContent = '0x32Cc052782E3A42Fcb25D7242E2FA5F8B0c3583B';
        ethAddress.style.cursor = 'pointer';
        ethAddress.addEventListener('click', function () {
            copyToClipboard(ethAddress.textContent);
            displayBalloon(menu, 'ERC-20/BEP-20 address copied to clipboard');
        });

        const advText = document.createElement('span');
        advText.style.fontSize = '11px';
        advText.innerHTML = '<br>USD Advcash: ';
        advText.style.marginRight = '0px';

        const advAddress = document.createElement('span');
        advAddress.textContent = 'U 9415 5458 0453';
        advAddress.style.cursor = 'pointer';
        advAddress.addEventListener('click', function () {
            copyToClipboard(advAddress.textContent);
            displayBalloon(menu, 'Advcash address copied to clipboard');
        });

        buyCoffeeText.appendChild(trc20Text); trc20Text.appendChild(trc20Address);
        buyCoffeeText.appendChild(ethText); ethText.appendChild(ethAddress);
        buyCoffeeText.appendChild(advText); advText.appendChild(advAddress);
        hiddenPart.appendChild(buyCoffeeText);
        menu.appendChild(hiddenPart);
    }

    // Helper Function: Toggle Menu Extension
    function toggleMenuExtension(menu, hiddenPart) {
        const isExtended = menu.style.width === MENU_WIDTH_EXTENDED;
        applyThemeMenuStyles(menu, !isExtended);

        const newRightPosition = isExtended ? `-${BUY_COFFEE_WIDTH}` : '0';
        hiddenPart.style.right = newRightPosition;
    }

    // Helper Function: Update Theme Styles without Affecting Menu Extension
    function updateThemeStyles(menu) {
        const menuWidth = menu.style.width;
        applyThemeMenuStyles(menu);
        menu.style.width = menuWidth;
    }

    // Helper Function: Create Button Element
    function createButton(text, width, gridColumn, gridRow, clickHandler) {
        const button = document.createElement('button');
        button.textContent = text;
        button.style.width = width;
        button.style.gridColumn = gridColumn;
        button.style.gridRow = gridRow;
        button.addEventListener('click', clickHandler);
        return button;
    }

    // Main Function: Open Theme Menu
    function openThemeMenu() {
        const existingMenu = document.getElementById('themeMenu');

        // If the menu is already open, close it
        if (existingMenu) {
            document.body.removeChild(existingMenu);
            return;
        }

        // If the menu is not open, create and open it
        const menu = document.createElement('div');
        menu.id = 'themeMenu';
        applyThemeMenuStyles(menu);

        // Add a small button at the top right corner
        const toggleButton = createButton('☕', '32px', 'auto', 'auto', function () {
            toggleMenuExtension(menu);
        });
        toggleButton.style.position = 'absolute';
        toggleButton.style.top = '5px';
        toggleButton.style.right = '1px';
        toggleButton.style.background = 'transparent';
        toggleButton.style.border = 'none';
        toggleButton.style.padding = '0';
        toggleButton.style.cursor = 'pointer';
        toggleButton.style.zIndex = '1001';
        menu.appendChild(toggleButton);

        Object.entries(COLOR_GROUPS).forEach(([group, properties]) => {
            const option = document.createElement('div');
            option.style.display = 'flex';
            option.style.marginBottom = '4px';

            const label = document.createElement('label');
            label.textContent = `- ${TITLES[group]}`;
            label.style.marginRight = '4px';
            label.style.fontFamily = 'Calibri';
            label.style.fontSize = '14px';

            const colorPicker = document.createElement('input');
            colorPicker.type = 'color';
            colorPicker.value = savedColors[group] || DEFAULT_COLORS[group];
            colorPicker.style = COMMON_STYLES;
            colorPicker.setAttribute('data-group', group);
            colorPicker.addEventListener('input', function () {
                savedColors[group] = this.value;
                applyColors();
                updateThemeStyles(menu);
            });

            option.appendChild(colorPicker);
            option.appendChild(label);
            menu.appendChild(option);
        });
        const updateColorPickers = function () {
            Object.entries(COLOR_GROUPS).forEach(([group, properties]) => {
                const colorPicker = document.querySelector(`input[type="color"][data-group="${group}"]`);
                if (colorPicker) {
                    colorPicker.value = savedColors[group] || DEFAULT_COLORS[group];
                }
            });
        };

        // Flex container for importExportLabel and importExportInput
        const flexContainer = document.createElement('div');
        flexContainer.style.display = 'flex';
        flexContainer.style.flexDirection = 'column';
        flexContainer.style.marginBottom = '4px';

        const importExportInput = document.createElement('input');
        importExportInput.type = 'text';
        importExportInput.placeholder = 'theme settings here';
        importExportInput.style.border = '1px solid darkgrey';
        importExportInput.style.padding = '3px';
        importExportInput.style.background = 'darkgrey';
        importExportInput.style.color = '#222';
        importExportInput.style.borderRadius = '2px';
        importExportInput.style.fontFamily = 'Consolas';
        importExportInput.style.fontSize = '13px';
        importExportInput.style.zIndex = '1000';

        // Import/Export Section
        const importExportSection = document.createElement('div');
        importExportSection.style.display = 'grid';
        importExportSection.style.gridTemplateColumns = 'repeat(5, 1fr)';
        importExportSection.style.gridGap = '2px';
        importExportSection.style.marginBottom = '4px';

        const importButton = createButton('Import', '120px', '1 / span 1', '1', function () {
            const importedColors = importExportInput.value.trim();
            if (!importedColors) {
                // If the input field is empty
                displayBalloon(menu, 'Put settings in the field');
                return;
            }

            const importedArray = importedColors.split(/[\s-]+/);

            if (importedArray.length !== Object.keys(COLOR_GROUPS).length) {
                // If the input has incorrect number of color values
                displayBalloon(menu, 'Incorrect input');
                return;
            }

            // Check if all values are valid hex color codes
            const isValidHex = importedArray.every(color => /^#[0-9A-F]{6}$/i.test(color));

            if (isValidHex) {
                Object.keys(COLOR_GROUPS).forEach((group, index) => {
                    savedColors[group] = importedArray[index] || DEFAULT_COLORS[group];
                });
                applyColors();
                updateThemeStyles(menu);
                updateColorPickers();
                displayBalloon(menu, 'Theme imported');
            } else {
                // If any color value is not a valid hex code
                displayBalloon(menu, 'Incorrect input');
            }
        });

        // Import/Export Section: Export Button
        const exportButton = createButton('Export', '120px', '1 / span 1', '2', function () {
            const exportedColors = Object.keys(COLOR_GROUPS)
            .map((group) => savedColors[group] || DEFAULT_COLORS[group])
            .join(' ');
            importExportInput.value = exportedColors;
            copyToClipboard(exportedColors);
            // Show "Copied to clipboard" balloon
            displayBalloon(menu, 'Settings copied to clipboard');
        });

        // Save Button
        const saveButton = createButton('Save', '120px', '1 / span 1', '3', function () {
            GM_setValue('customColors', savedColors);
            applyColors();
            //document.body.removeChild(menu);
            // Show "Copied to clipboard" balloon
            displayBalloon(menu, 'Theme saved');
        });

        menu.appendChild(flexContainer);
        flexContainer.appendChild(importExportInput);
        menu.appendChild(importExportSection);
        importExportSection.appendChild(importButton);
        importExportSection.appendChild(exportButton);

        // Empty column
        const importExportEmptyColumn = document.createElement('label');
        importExportEmptyColumn.style.gridColumn = '2 / span 1';
        importExportEmptyColumn.style.minWidth = '146px';
        importExportSection.appendChild(importExportEmptyColumn);

        // Preset Buttons - Save
        for (let i = 1; i <= 3; i++) {
            const saveCustomButton = createButton(`💾 Custom ${i}`, '100px', `${i + 2} / span 1`, '1', function () {
                savePreset(i);
                displayBalloon(menu, `Preset saved as Custom ${i}`);
            });
            importExportSection.appendChild(saveCustomButton);
        }

        // Preset Buttons - Load
        for (let i = 1; i <= 3; i++) {
            const loadCustomButton = createButton(`🔼 Custom ${i}`, '100px', `${i + 2} / span 1`, '2', function () {
                loadPreset(i);
                displayBalloon(menu, `Custom preset ${i} loaded`);
            });
            importExportSection.appendChild(loadCustomButton);
        }

        // Preset Buttons - Presets
        const presets = [
            { label: '🔼 Preset 1', width: '100px', gridColumn: '3 / span 1', gridRow: '3', colors: DEFAULT_COLORS },
            { label: '🔼 Preset 2', width: '100px', gridColumn: '4 / span 1', gridRow: '3', colors: PRESET_COLORS_2 },
            { label: '🔼 Preset 3', width: '100px', gridColumn: '5 / span 1', gridRow: '3', colors: PRESET_COLORS_3 },
        ];

        presets.forEach((preset) => {
            const presetButton = createButton(preset.label, preset.width, preset.gridColumn, preset.gridRow, function () {
                Object.keys(COLOR_GROUPS).forEach((group) => {
                    savedColors[group] = preset.colors[group];
                });
                applyColors();
                updateThemeStyles(menu);
                updateColorPickers();
                displayBalloon(menu, `${preset.label.substring(2)} loaded`);
            });
            importExportSection.appendChild(presetButton);
        });

        // Helper Function: Save Preset
        function savePreset(presetNumber) {
            const presetKey = `customPreset${presetNumber}`;
            GM_setValue(presetKey, savedColors);
        }

        // Helper Function: Load Preset
        function loadPreset(presetNumber) {
            const presetKey = `customPreset${presetNumber}`;
            const presetColors = GM_getValue(presetKey);
            Object.keys(COLOR_GROUPS).forEach((group) => {
                savedColors[group] = presetColors[group];
            });
            applyColors();
            updateThemeStyles(menu);
            updateColorPickers();
        }
        // Create a new paragraph element
        const paragraph = document.createElement('p');

        // Set the text content of the paragraph
        paragraph.innerHTML = 'TradingView<br>Theme<br>Customizer<br><br>v1.0';

        // Style the paragraph if needed
        paragraph.style.fontFamily = 'Consolas';
        paragraph.style.fontSize = '13px';
        paragraph.style.color = savedColors.color4 || DEFAULT_COLORS.color4;
        paragraph.style.position = 'absolute';
        paragraph.style.bottom = '16px';
        paragraph.style.left = '140px';

        // Append the paragraph to the menu
        menu.appendChild(paragraph);

        importExportSection.appendChild(saveButton);
        document.body.appendChild(menu);
    }

    // Helper Function: Add Theme Button
    function addThemeButton() {
        const button = document.createElement('button');
        button.style.position = 'fixed';
        button.style.top = '5px';
        button.style.left = '12px';
        button.style.background = 'transparent';
        button.style.padding = '0';
        button.style.cursor = 'pointer';
        button.style.borderLeft = `5px solid ${savedColors.color4 || DEFAULT_COLORS.color4}`;
        button.style.borderTop = `5px solid ${savedColors.color4 || DEFAULT_COLORS.color4}`;
        button.style.borderRight = '5px solid transparent';
        button.style.borderBottom = '5px solid transparent';

        // Hover styles
        button.addEventListener('mouseover', function () {
            button.style.borderLeft = `5px solid ${savedColors.color5 || DEFAULT_COLORS.color5}`;
            button.style.borderTop = `5px solid ${savedColors.color5 || DEFAULT_COLORS.color5}`;
        });

        button.addEventListener('mouseout', function () {
            button.style.borderLeft = `5px solid ${savedColors.color4 || DEFAULT_COLORS.color4}`;
            button.style.borderTop = `5px solid ${savedColors.color4 || DEFAULT_COLORS.color4}`;
        });

        button.addEventListener('click', openThemeMenu);
        document.body.appendChild(button);
    }

    // Main Execution
    applyColors();
    addThemeButton();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址