Skip to content

Instantly share code, notes, and snippets.

@alekxeyuk
Last active April 12, 2026 16:45
Show Gist options
  • Select an option

  • Save alekxeyuk/6961a1c5dc94566a3a1f19c08b4fe706 to your computer and use it in GitHub Desktop.

Select an option

Save alekxeyuk/6961a1c5dc94566a3a1f19c08b4fe706 to your computer and use it in GitHub Desktop.
translationchicken.com Color dialogue lines
// ==UserScript==
// @name TranslationChicken Character Colors + UI
// @namespace https://translationchicken.com
// @version 2.4.3
// @description Color dialogue lines with UI controls
// @author Prostagma
// @match https://translationchicken.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=translationchicken.com
// @homepageURL https://gist.github.com/alekxeyuk/6961a1c5dc94566a3a1f19c08b4fe706
// @downloadURL https://gist.githubusercontent.com/alekxeyuk/6961a1c5dc94566a3a1f19c08b4fe706/raw/userscript.js
// @updateURL https://gist.githubusercontent.com/alekxeyuk/6961a1c5dc94566a3a1f19c08b4fe706/raw/userscript.js
// @run-at document-idle
// @grant none
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = "tc_character_colors";
// ================================
// DEFAULT CONFIG
// ================================
const defaultConfig = {
"Otto": { text: "green", bg: "transparent", enabled: true },
"Subaru": { text: "yellow", bg: "transparent", enabled: true }
};
let config = loadConfig();
function loadConfig() {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : JSON.parse(JSON.stringify(defaultConfig));
}
function saveConfig() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
}
// ================================
// UTILITIES
// ================================
// Debounce prevents the observer from firing too rapidly
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Reusable canvas context for hex conversion
const _ctx = document.createElement("canvas").getContext("2d");
function toHex(color) {
if (!color || color === 'transparent') return '#000000';
_ctx.fillStyle = '#000000'; // reset
_ctx.fillStyle = color;
return _ctx.fillStyle; // automatically returns hex
}
function getElementByXPath(xpath) {
return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
function getContainer() {
// Try a robust CSS selector first (adjust class names to match the site)
let container = document.querySelector('article .entry-content')
|| document.querySelector('article .post-content');
// Fallback to the original XPath if CSS fails
if (!container) {
container = getElementByXPath('/html/body/div[2]/main/div/article/div[2]');
}
return container;
}
// ================================
// APPLY COLORS
// ================================
function applyColors() {
const container = getContainer();
if (!container) return;
const paragraphs = container.querySelectorAll('p');
paragraphs.forEach(p => {
const text = p.textContent.trim();
const match = text.match(/^\s*[\[\{]([^\]]+?):/);
if (!match) return;
const character = match[1].trim();
const charConfig = config[character];
let newColor = "";
let newBg = "";
if (charConfig && charConfig.enabled) {
newColor = charConfig.text;
newBg = charConfig.bg;
}
// Only update the DOM if the style actually changed (prevents infinite MutationObserver loops)
if (p.style.color !== newColor) p.style.color = newColor;
if (p.style.backgroundColor !== newBg) p.style.backgroundColor = newBg;
});
}
const debouncedApply = debounce(applyColors, 100);
// ================================
// OBSERVER
// ================================
function observeChanges() {
const observer = new MutationObserver(debouncedApply);
// Observe the whole body for sub-tree changes. Debouncing makes this safe performance-wise.
observer.observe(document.body, { childList: true, subtree: true });
}
// ================================
// UI PANEL
// ================================
function createPanel() {
const style = document.createElement('style');
style.textContent = `
#tc-panel { position: fixed; top: 20px; right: 20px; z-index: 9999; background: #222; color: #fff; padding: 10px; border-radius: 8px; font-size: 12px; max-width: 300px; max-height: calc(100vh - 40px); font-family: sans-serif; box-shadow: 0 4px 6px rgba(0,0,0,0.3); display: flex; flex-direction: column; }
#tc-panel.minimized #tc-content { display: none; }
#tc-panel-header { cursor: pointer; font-weight: bold; margin-bottom: 8px; user-select: none; display: flex; justify-content: space-between; flex-shrink: 0; }
#tc-content { display: flex; flex-direction: column; overflow: hidden; }
#tc-list { overflow-y: auto; max-height: calc(100vh - 180px); padding-right: 5px; scrollbar-width: thin; scrollbar-color: #666 #333; }
#tc-list::-webkit-scrollbar { width: 6px; }
#tc-list::-webkit-scrollbar-track { background: #333; border-radius: 3px; }
#tc-list::-webkit-scrollbar-thumb { background: #666; border-radius: 3px; }
#tc-list::-webkit-scrollbar-thumb:hover { background: #888; }
.tc-row { margin-bottom: 8px; border-bottom: 1px solid #444; padding-bottom: 6px; }
.tc-row label { font-weight: bold; display: block; margin-bottom: 4px; }
.tc-row input[type="color"] { vertical-align: middle; cursor: pointer; width: 25px; height: 25px; border: none; padding: 0; }
.tc-row button { cursor: pointer; background: #555; color: white; border: none; border-radius: 3px; float: right; }
.tc-row button:hover { background: #777; }
.tc-bg-options { margin-top: 4px; display: flex; justify-content: space-between; align-items: center; }
#tc-footer { margin-top: 10px; display: flex; gap: 8px; flex-direction: column; flex-shrink: 0; }
#tc-footer button { flex: 1; background: #444; color: white; border: none; border-radius: 4px; padding: 5px; cursor: pointer; }
#tc-footer button:hover { background: #666; }
`;
document.head.appendChild(style);
const panel = document.createElement('div');
panel.id = 'tc-panel';
panel.className = 'minimized';
panel.innerHTML = `
<div id="tc-panel-header">
<span>Character Colors</span>
<span id="tc-toggle">▲</span>
</div>
<div id="tc-content">
<div id="tc-list"></div>
<div id="tc-footer">
<button id="tc-add">Add Character</button>
<button id="tc-reset">Reset Defaults</button>
<button id="tc-import-export">Import/Export</button>
</div>
</div>
`;
// Toggle minimize
panel.querySelector('#tc-panel-header').onclick = () => {
panel.classList.toggle('minimized');
panel.querySelector('#tc-toggle').textContent = panel.classList.contains('minimized') ? '▲' : '▼';
};
const list = panel.querySelector('#tc-list');
function render() {
list.innerHTML = '';
Object.keys(config).forEach(name => {
const entry = config[name];
const isBgTransparent = entry.bg === 'transparent';
const row = document.createElement('div');
row.className = 'tc-row';
row.innerHTML = `
<label>
<input type="checkbox" class="tc-enabled" ${entry.enabled ? 'checked' : ''}>
${name}
</label>
<div class="tc-bg-options">
Text: <input type="color" class="tc-text" value="${toHex(entry.text)}">
BG: <input type="color" class="tc-bg-color" value="${isBgTransparent ? '#000000' : toHex(entry.bg)}">
<button class="tc-delete" title="Remove">✕</button>
</div>
`;
// Safer event binding using classes instead of array indexing
row.querySelector('.tc-enabled').onchange = (e) => {
entry.enabled = e.target.checked;
saveConfig(); debouncedApply();
};
row.querySelector('.tc-text').oninput = (e) => {
entry.text = e.target.value;
saveConfig(); debouncedApply();
};
row.querySelector('.tc-bg-color').oninput = (e) => {
entry.bg = e.target.value;
saveConfig(); debouncedApply();
};
row.querySelector('.tc-delete').onclick = () => {
delete config[name];
saveConfig(); render(); debouncedApply();
};
list.appendChild(row);
});
}
// Add new character
panel.querySelector('#tc-add').onclick = () => {
const name = prompt("Character name:");
if (!name) return;
config[name] = { text: "#ffffff", bg: "transparent", enabled: true };
saveConfig(); render();
};
// Reset to defaults
panel.querySelector('#tc-reset').onclick = () => {
if (confirm("Reset all colors to defaults?")) {
config = JSON.parse(JSON.stringify(defaultConfig));
saveConfig(); render(); debouncedApply();
}
};
// Import or Export config
panel.querySelector('#tc-import-export').onclick = () => {
const exportData = JSON.stringify(config, null, 2);
const isExport = prompt("Export config (copy below):\n\n" + exportData, exportData);
if (isExport && isExport !== exportData) { // User pasted something
try {
const imported = JSON.parse(isExport);
// Validate structure
Object.keys(imported).forEach(key => {
if (typeof imported[key] === 'object' &&
'text' in imported[key] &&
'bg' in imported[key] &&
'enabled' in imported[key]) {
config[key] = imported[key];
}
});
saveConfig();
render();
applyColors();
alert('Import successful!');
} catch (e) {
alert('Invalid config format');
}
}
};
document.body.appendChild(panel);
render();
}
// ================================
// INIT
// ================================
function init() {
createPanel();
observeChanges();
applyColors();
}
init();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment