// ==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>\
/ o o \\<br>\
( == ^ == )<br>\
) (<br>\
( )<br>\
( ( ) ( ) )<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();
})();