Last active
April 12, 2026 16:45
-
-
Save alekxeyuk/6961a1c5dc94566a3a1f19c08b4fe706 to your computer and use it in GitHub Desktop.
translationchicken.com Color dialogue lines
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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