Drawaria No Censor Words

Bypass Drawaria.online censorship by converting text to bold Unicode characters.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Drawaria No Censor Words
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Bypass Drawaria.online censorship by converting text to bold Unicode characters.
// @author       YouTubeDrawaria
// @match        https://drawaria.online/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // A simple utility to create DOM elements, ensuring the script is truly standalone.
    const domMake = {
        Tree: function(tagName, attributes = {}, children = []) {
            const element = document.createElement(tagName);
            for (const key in attributes) {
                if (attributes.hasOwnProperty(key)) {
                    if (key === 'style' && typeof attributes[key] === 'string') {
                        element.style.cssText = attributes[key];
                    } else if (key in element && tagName !== 'input') { // Avoid setting value attribute as property for inputs
                        element[key] = attributes[key];
                    } else {
                        element.setAttribute(key, attributes[key]);
                    }
                }
            }
            children.forEach(child => {
                if (typeof child === 'string') {
                    element.appendChild(document.createTextNode(child));
                } else if (child instanceof Node) {
                    element.appendChild(child);
                }
            });
            return element;
        },
        Button: function(text, attributes = {}) {
            const button = document.createElement('button');
            button.textContent = text;
            for (const key in attributes) {
                if (attributes.hasOwnProperty(key)) {
                    if (key === 'style' && typeof attributes[key] === 'string') {
                        button.style.cssText = attributes[key];
                    } else {
                        button.setAttribute(key, attributes[key]);
                    }
                }
            }
            return button;
        }
    };


    // --- Character map for Bold Text ---
    const BOLD_TEXT_MAP = {
        'A': '𝗔', 'B': '𝗕', 'C': '𝗖', 'D': '𝗗', 'E': '𝗘', 'F': '𝗙', 'G': '𝗚', 'H': '𝗛', 'I': '𝗜', 'J': '𝗝', 'K': '𝗞', 'L': '𝗟', 'M': '𝗠', 'N': '𝗡', 'O': '𝗢', 'P': '𝗣', 'Q': '𝗤', 'R': '𝗥', 'S': '𝗦', 'T': '𝗧', 'U': '𝗨', 'V': '𝗩', 'W': '𝗪', 'X': '𝗫', 'Y': '𝗬', 'Z': '𝗭', 'a': '𝗮', 'b': '𝗯', 'c': '𝗰', 'd': '𝗱', 'e': '𝗲', 'f': '𝗳', 'g': '𝗴', 'h': '𝗵', 'i': '𝗶', 'j': '𝗷', 'k': '𝗸', 'l': '𝗹', 'm': '𝗺', 'n': '𝗻', 'o': '𝗼', 'p': '𝗽', 'q': '𝗾', 'r': '𝗿', 's': '𝘀', 't': '𝘁', 'u': '𝘂', 'v': '𝘃', 'w': '𝘄', 'x': '𝘅', 'y': '𝘆', 'z': '𝘇', '0': '𝟬', '1': '𝟭', '2': '𝟮', '3': '𝟯', '4': '𝟰', '5': '𝟱', '6': '𝟲', '7': '𝟳', '8': '𝟴', '9': '𝟵'
    };

    class NoCensorWords {
        #panel = null;
        #toggleButton = null;
        #isCensorBypassActive = false;
        #chatInputObserver = null;
        #currentChatInput = null; // Reference to the currently active chat input element
        #inputListener = null; // Store the listener reference for proper removal
        #observerTarget = null;
        #_attachInputDebounceTimer = null; // Private property for the debounce timer

        constructor() {
            console.log("NoCensorWords: Initializing...");
            this.#addStyles();
            this.#createPanel();
            this.#setupChatInputMonitoring();
            console.log("NoCensorWords: Initialization complete.");
        }

        #addStyles() {
            const style = document.createElement('style');
            style.textContent = `
                .no-censor-button {
                    background: #f0f0f0; /* Light background */
                    color: #333; /* Dark text */
                    border: 1px solid #ccc;
                    border-radius: 5px;
                    padding: 8px 12px;
                    font-size: 1em;
                    cursor: pointer;
                    transition: background 0.16s ease-in-out, color 0.16s ease-in-out;
                    width: 100%; /* Make button fill panel width */
                    box-sizing: border-box; /* Include padding and border in width */
                }
                .no-censor-button:hover {
                    background: #e0e0e0; /* Slightly darker on hover */
                }
                .no-censor-button.active {
                    background: #4CAF50; /* Green for active */
                    border-color: #4CAF50;
                    color: #fff; /* White text for active */
                    font-weight: bold;
                }
                .no-censor-button.active:hover {
                    background: #5cb85c;
                }
                .no-censor-panel {
                    position: fixed;
                    top: 100px; /* Default position */
                    right: 20px;
                    width: 220px; /* Adjusted width to be more compact */
                    background: #ffffff; /* White background for the panel */
                    border: 1px solid #ddd;
                    border-radius: 8px;
                    box-shadow: 0 4px 12px rgba(0,0,0,0.2);
                    z-index: 100000; /* High z-index to be on top */
                    color: #333; /* Default text color for panel content */
                    font-family: Arial, sans-serif;
                    padding: 10px;
                    box-sizing: border-box;
                    user-select: none; /* Prevent text selection on the panel itself */
                }
                .no-censor-panel-header {
                    cursor: grab;
                    padding: 5px;
                    background: #f0f0f0; /* Light header background */
                    border-bottom: 1px solid #e0e0e0;
                    margin: -10px -10px 10px -10px; /* Negative margin to pull it to edges */
                    border-top-left-radius: 8px;
                    border-top-right-radius: 8px;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    user-select: none; /* Prevent text selection on the header */
                }
                .no-censor-panel-header span {
                    font-weight: bold;
                    font-size: 1.1em;
                    margin-left: 10px;
                    color: #333; /* Dark text for title */
                }
                .no-censor-panel-close-btn {
                    background: none;
                    border: none;
                    color: #666; /* Darker close button color */
                    font-size: 1.5em;
                    cursor: pointer;
                    padding: 0 5px;
                }
                .no-censor-panel-close-btn:hover {
                    color: #ff0000;
                }
            `;
            document.head.appendChild(style);
            console.log("NoCensorWords: Styles added.");
        }

        #createPanel() {
            this.#panel = domMake.Tree('div', { class: 'no-censor-panel' });

            const header = domMake.Tree('div', { class: 'no-censor-panel-header' });
            const title = domMake.Tree('span', {}, ["No Censor Words"]);
            const closeButton = domMake.Button('×', { class: 'no-censor-panel-close-btn' });
            closeButton.onclick = () => {
                this.#panel.remove();
                if (this.#chatInputObserver) {
                    this.#chatInputObserver.disconnect();
                    console.log("NoCensorWords: MutationObserver disconnected on panel close.");
                }
                if (this.#currentChatInput && this.#inputListener) {
                    this.#currentChatInput.removeEventListener('input', this.#inputListener);
                    console.log("NoCensorWords: Chat input listener removed on panel close.");
                }
                console.log("NoCensorWords: Panel removed.");
            };

            // FIX: Use standard appendChild instead of custom appendAll
            header.appendChild(title);
            header.appendChild(closeButton);

            this.#panel.appendChild(header);

            this.#toggleButton = domMake.Button('Activar Bypass', { class: 'no-censor-button' });
            this.#toggleButton.onclick = () => this.#toggleCensorBypass();
            this.#panel.appendChild(this.#toggleButton);

            document.body.appendChild(this.#panel);
            this.#makeDraggable(this.#panel, header);
            console.log("NoCensorWords: Panel created and appended to body.");
        }

        #makeDraggable(element, handle) {
            let offsetX, offsetY;
            let isDragging = false;

            const startDrag = (e) => {
                // Ensure event target is the handle itself, not a child of the handle
                if (e.target !== handle && e.target !== handle.firstElementChild) {
                    // Check if the target is the close button, don't drag if so
                    if (e.target.classList.contains('no-censor-panel-close-btn')) {
                        return;
                    }
                }

                e.preventDefault(); // Prevent default browser drag behavior

                const rect = element.getBoundingClientRect();
                offsetX = e.clientX - rect.left;
                offsetY = e.clientY - rect.top;

                isDragging = true;
                handle.style.cursor = 'grabbing';
                document.body.style.userSelect = 'none'; // Prevent text selection on body

                document.addEventListener('mousemove', doDrag);
                document.addEventListener('mouseup', stopDrag);
            };

            const doDrag = (e) => {
                if (!isDragging) return;
                e.preventDefault();

                let newLeft = e.clientX - offsetX;
                let newTop = e.clientY - offsetY;

                // Clamp to viewport boundaries
                newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - element.offsetWidth));
                newTop = Math.max(0, Math.min(newTop, window.innerHeight - element.offsetHeight));

                element.style.left = `${newLeft}px`;
                element.style.top = `${newTop}px`;
            };

            const stopDrag = () => {
                isDragging = false;
                handle.style.cursor = 'grab';
                document.body.style.userSelect = 'auto'; // Restore text selection

                document.removeEventListener('mousemove', doDrag);
                document.removeEventListener('mouseup', stopDrag);
            };

            handle.addEventListener('mousedown', startDrag);
            handle.style.cursor = 'grab'; // Set default cursor for handle
            console.log("NoCensorWords: Panel made draggable.");
        }

        #toggleCensorBypass() {
            this.#isCensorBypassActive = !this.#isCensorBypassActive;
            if (this.#isCensorBypassActive) {
                this.#toggleButton.textContent = 'Desactivar Bypass';
                this.#toggleButton.classList.add('active');
                this.#applyConversionToCurrentInput(); // Apply to existing text if any
                console.log("NoCensorWords: Censura bypass ACTIVADO.");
            } else {
                this.#toggleButton.textContent = 'Activar Bypass';
                this.#toggleButton.classList.remove('active');
                // Reverting text is complex, simply stop future conversions.
                // The user would need to retype if they want original characters.
                console.log("NoCensorWords: Censura bypass DESACTIVADO.");
            }
        }

        #getChatInput() {
            // This attempts to get the chat input. It targets the id used by Drawaria,
            // which could be the original input or a textarea replaced by other scripts.
            return document.querySelector('#chatbox_textinput');
        }

        #setupChatInputMonitoring() {
            // Find a stable parent element to observe for chat input changes
            this.#observerTarget = document.querySelector('#chatattop') || document.body;
            if (!this.#observerTarget) {
                console.error("NoCensorWords: Could not find chat container or body to observe for input changes.");
                return;
            }

            this.#chatInputObserver = new MutationObserver(() => {
                // Debounce the attachment to avoid multiple calls on rapid DOM changes
                clearTimeout(this.#_attachInputDebounceTimer); // Use the private property
                this.#_attachInputDebounceTimer = setTimeout(() => { // Assign to private property
                    this.#attachInputListener();
                }, 100); // Small delay to let DOM settle
            });

            // Observe for changes in the chat container or body for the input element
            this.#chatInputObserver.observe(this.#observerTarget, { childList: true, subtree: true, attributes: false });
            console.log("NoCensorWords: MutationObserver set up on", this.#observerTarget === document.body ? "body" : "#chatattop");

            // Initial attachment in case input is already present when script loads
            this.#attachInputListener();
        }

        #attachInputListener() {
            const newChatInput = this.#getChatInput();

            if (newChatInput && newChatInput !== this.#currentChatInput) {
                // Remove old listener if it exists and is different from new input
                if (this.#currentChatInput && this.#inputListener) {
                    this.#currentChatInput.removeEventListener('input', this.#inputListener);
                    console.log("NoCensorWords: Removed old chat input listener.");
                }

                this.#currentChatInput = newChatInput;
                this.#inputListener = (event) => this.#handleChatInput(event);
                this.#currentChatInput.addEventListener('input', this.#inputListener);
                console.log("NoCensorWords: Attached new chat input listener to:", this.#currentChatInput);
                this.#applyConversionToCurrentInput(); // Apply conversion immediately upon attaching
            } else if (!newChatInput && this.#currentChatInput) {
                // Input disappeared
                this.#currentChatInput.removeEventListener('input', this.#inputListener);
                this.#currentChatInput = null;
                this.#inputListener = null;
                console.log("NoCensorWords: Chat input disappeared, listener removed.");
            } else if (newChatInput === this.#currentChatInput) {
                // console.log("NoCensorWords: Chat input is the same, no re-attachment needed.");
            } else {
                // console.log("NoCensorWords: Chat input not found yet.");
            }
        }

        #handleChatInput(event) {
            if (!this.#isCensorBypassActive || !this.#currentChatInput) {
                return;
            }

            const currentText = this.#currentChatInput.value;
            const originalSelectionStart = this.#currentChatInput.selectionStart;
            const originalSelectionEnd = this.#currentChatInput.selectionEnd;

            let convertedText = '';
            let charOffset = 0; // Tracks the change in length due to conversion

            for (let i = 0; i < currentText.length; i++) {
                const originalChar = currentText[i];
                // Convert uppercase and lowercase letters, and digits
                const convertedChar = BOLD_TEXT_MAP[originalChar] || BOLD_TEXT_MAP[originalChar.toUpperCase()] || originalChar;

                convertedText += convertedChar;

                // Adjust charOffset if character length changes
                if (convertedChar.length !== originalChar.length) {
                    charOffset += (convertedChar.length - originalChar.length);
                }
            }

            // Only update if conversion actually changed the text to prevent infinite loops
            if (this.#currentChatInput.value !== convertedText) {
                // FIX: Directly assign the value. This is the safest way to update.
                this.#currentChatInput.value = convertedText;

                // Restore cursor and selection, adjusting for length changes
                this.#currentChatInput.selectionStart = originalSelectionStart + charOffset;
                this.#currentChatInput.selectionEnd = originalSelectionEnd + charOffset;

                // Optionally, dispatch an input event manually after setting value
                // if framework relies on it, though direct .value usually doesn't trigger 'input'
                // and we already have a wrapper dispatching in #applyConversionToCurrentInput.
                // No need to dispatch here unless required by a specific framework.
            }
        }

        // Applies conversion to the current input value immediately when toggled on
        #applyConversionToCurrentInput() {
            if (this.#currentChatInput && this.#isCensorBypassActive) {
                // Dispatch a synthetic 'input' event to trigger #handleChatInput
                // This is crucial because directly setting .value doesn't fire 'input'.
                const inputEvent = new Event('input', { bubbles: true });
                this.#currentChatInput.dispatchEvent(inputEvent);
            }
        }
    }

    // Initialize the module
    function initNoCensorWordsScript() {
        console.log("NoCensorWords: DOMContentLoaded or script executed.");
        // Add a small delay to ensure the main Drawaria chat input is ready,
        // especially if other scripts (like AdvancedChatEnhancements) are also modifying it.
        setTimeout(() => {
            new NoCensorWords();
        }, 1500); // 1.5 seconds delay
    }

    // Ensure the script runs after the DOM is fully loaded
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initNoCensorWordsScript);
    } else {
        initNoCensorWordsScript();
    }

})();