Blungs-Tools for fishtank.live

Script for toggling features.

// ==UserScript==
// @name         Blungs-Tools for fishtank.live
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  Script for toggling features.
// @author       Blungs
// @match        https://*.fishtank.live/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=fishtank.live
// @license      MIT
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    const state = {
        chatFilter: true,
        avatars: true,
        timestamps: true,
        itemsUsed: true,
        ad: true,
        resize: true,
        top: true,
        left: true,
        right: true,
        stocks: true,
        bottom: true
    };

    const CONSECUTIVE_LIMIT = 4;

    function filterRepeatedChars(text) {
        return text.replace(/(\w)\1{3,}/g, (match, char) => char.repeat(CONSECUTIVE_LIMIT));
    }

    function filterRepeatedPhrases(text) {
        let cleaned = text.trim();
        let changed = true;

        while (changed) {
            changed = false;

            // Repeated word sequences
            const words = cleaned.split(/\s+/);
            for (let size = 6; size >= 2; size--) {
                for (let i = 0; i <= words.length - size * 2; i++) {
                    const phrase = words.slice(i, i + size).join(' ');
                    const next = words.slice(i + size, i + size * 2).join(' ');
                    if (phrase.toLowerCase() === next.toLowerCase()) {
                        cleaned = words.slice(0, i + size).join(' ') + ' ...';
                        changed = true;
                        break;
                    }
                }
                if (changed) break;
            }

            // Repeated character blocks (e.g., EXAMPLEEXAMPLEEXAMPLE)
            if (!changed) {
                for (let size = Math.floor(cleaned.length / 2); size >= 4; size--) {
                    const block = cleaned.slice(0, size);
                    const escaped = block.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
                    const regex = new RegExp(`^(?:${escaped}){2,}`, 'i');
                    if (regex.test(cleaned)) {
                        cleaned = block + '...';
                        changed = true;
                        break;
                    }
                }
            }
        }

        return cleaned;
    }

    function processSpan(span) {
        if (!span || span.dataset.filteredProcessed === "true") return;
        const original = span.textContent.trim();
        if (!original) return;
        let filtered = filterRepeatedChars(original);
        filtered = filterRepeatedPhrases(filtered);
        if (original !== filtered) {
            const wrapper = document.createElement("span");
            wrapper.className = "filtered-spam-message";
            wrapper.dataset.filteredProcessed = "true";
            wrapper.textContent = filtered;
            wrapper.title = original;
            span.style.display = "none";
            span.after(wrapper);
        }
    }

    let chatFilterObserver;
    function observeChatFilter() {
        const chat = document.getElementById('chat-messages');
        if (!chat) return setTimeout(observeChatFilter, 1000);
        if (chatFilterObserver) chatFilterObserver.disconnect();
        chatFilterObserver = new MutationObserver(mutations => {
            if (!state.chatFilter) return;
            mutations.forEach(m => {
                m.addedNodes.forEach(node => {
                    if (node.nodeType !== 1) return;
                    if (node.tagName === 'SPAN') {
                        processSpan(node);
                    } else {
                        node.querySelectorAll('span').forEach(processSpan);
                    }
                });
            });
        });
        chatFilterObserver.observe(chat, { childList: true, subtree: true });
        if (state.chatFilter) {
            chat.querySelectorAll('span').forEach(processSpan);
        }
    }

    const avatarSelector = '.chat-message-default_avatar__eVmdi';
    function removeAvatars() {
        if (!state.avatars) return;
        const avatars = document.querySelectorAll(avatarSelector);
        avatars.forEach(el => el.remove());
    }

    const avatarObserver = new MutationObserver(() => {
        if (state.avatars) removeAvatars();
    });
    avatarObserver.observe(document.body, { childList: true, subtree: true });

    function removeTimestamps() {
        if (!state.timestamps) return;
        const timestamps = document.querySelectorAll('.chat-message-default_timestamp__sGwZy');
        timestamps.forEach(el => el.remove());
    }

    function removeUsedItems() {
        if (!state.itemsUsed) return;
        const usedItems = document.querySelectorAll('[class^="chat-message-happening_item__mi9tp"]');
        usedItems.forEach(el => el.remove());
    }

    const cleanupObserver = new MutationObserver(() => {
        removeTimestamps();
        removeUsedItems();
    });
    cleanupObserver.observe(document.body, { childList: true, subtree: true });

    function applyResizeStyle(enabled) {
        const existing = document.getElementById('resize-style');
        if (existing) existing.remove();
        if (!enabled) return;
        const style = document.createElement('style');
        style.id = 'resize-style';
        style.textContent = `
            #chat-messages {
                display: flex !important;
                flex-direction: column !important;
                gap: 0.2em !important;
            }
            #chat-messages * {
                font-size: 0.95em !important;
                line-height: 1em !important;
                margin: 0 !important;
            }
        `;
        document.head.appendChild(style);
    }

    function toggleVisibility(className, show) {
        document.querySelectorAll(`.${className.split(' ').join('.')}`).forEach(el => {
            el.style.display = show ? '' : 'none';
        });
    }

    function toggleAds() {
        toggleVisibility('ads_ads__Z1cPk', state.ad);
    }

    const functionList = {
        'Chat Filter': {
            get enabled() { return state.chatFilter; },
            action() {
                state.chatFilter = !state.chatFilter;
                if (state.chatFilter) observeChatFilter();
                else if (chatFilterObserver) chatFilterObserver.disconnect();
            }
        },
        'Avatars': {
            get enabled() { return state.avatars; },
            action() {
                state.avatars = !state.avatars;
                if (state.avatars) removeAvatars();
            }
        },
        'Timestamps': {
            get enabled() { return state.timestamps; },
            action() {
                state.timestamps = !state.timestamps;
                if (state.timestamps) removeTimestamps();
            }
        },
        'Items Used': {
            get enabled() { return state.itemsUsed; },
            action() {
                state.itemsUsed = !state.itemsUsed;
                if (state.itemsUsed) removeUsedItems();
            }
        },
        'Ad': {
            get enabled() { return state.ad; },
            action() {
                state.ad = !state.ad;
                toggleAds();
            }
        },
        'Resize': {
            get enabled() { return state.resize; },
            action() {
                state.resize = !state.resize;
                applyResizeStyle(state.resize);
            }
        },
        'Top': {
            get enabled() { return state.top; },
            action() {
                state.top = !state.top;
                toggleVisibility('layout_top__MHaU_', state.top);
            }
        },
        'Left': {
            get enabled() { return state.left; },
            action() {
                state.left = !state.left;
                toggleVisibility('layout_left__O2uku', state.left);
            }
        },
        'Right': {
            get enabled() { return state.right; },
            action() {
                state.right = !state.right;
                toggleVisibility('chat_chat__2rdNg', state.right);
            }
        },
        'Stocks Bar': {
            get enabled() { return state.stocks; },
            action() {
                state.stocks = !state.stocks;
                toggleVisibility('stocks-bar_stocks-bar__7kNv8', state.stocks);
            }
        },
        'Bottom': {
            get enabled() { return state.bottom; },
            action() {
                state.bottom = !state.bottom;
                toggleVisibility('layout_center-bottom__yhDOH', state.bottom);
            }
        }
    };

    const chatFunctions = ['Chat Filter', 'Avatars', 'Timestamps', 'Items Used', 'Resize'];
    const layoutFunctions = Object.keys(functionList).filter(f => !chatFunctions.includes(f));

    const menu = document.createElement('div');
    menu.id = 'q-toggle-menu';
    Object.assign(menu.style, {
        position: 'fixed',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)',
        background: '#222',
        color: '#fff',
        padding: '20px',
        border: '1px solid #555',
        borderRadius: '8px',
        display: 'none',
        zIndex: '9999',
        fontFamily: 'Arial, sans-serif',
        fontSize: '180%',
        boxShadow: '0 0 10px rgba(0,0,0,0.5)',
        userSelect: 'none',
    });
    document.body.appendChild(menu);

    function updateMenu() {
        menu.innerHTML = `
            <div style="position: relative; min-height: 25px;">
                <span id="close-q-menu" style="
                    position: absolute;
                    top: 8px;
                    right: 8px;
                    cursor: pointer;
                    color: red;
                    font-size: 140%;
                    background: #000;
                    padding: 4px 6px;
                    border-radius: 50%;
                    line-height: 0;
                ">✖</span>
            </div>
            <div style="padding-top: 10px; margin-bottom: 30px; text-align: center;">
                <strong style="color: limegreen; display: inline-block;">Functions Menu</strong>
            </div>
            <div style="display: flex; gap: 40px;">
                <div>
                    <strong style="text-decoration: underline; color: deepskyblue;">Chat</strong>
                    ${chatFunctions.map(key => {
                        const value = functionList[key].enabled;
                        const isInverted = ['Avatars', 'Timestamps', 'Items Used'].includes(key);
                        const status = isInverted ? (value ? 'Off' : 'On') : (value ? 'On' : 'Off');
                        return `
                            <div style="cursor: pointer; padding: 18px 0; border-bottom: 1px solid #444;" onclick="toggleFunction('${key}')">
                                ${key}: ${status}
                            </div>
                        `;
                    }).join('')}
                </div>
                <div>
                    <strong style="text-decoration: underline; color: deepskyblue;">Site Layout</strong>
                    ${layoutFunctions.map(key => `
                        <div style="cursor: pointer; padding: 18px 0; border-bottom: 1px solid #444;" onclick="toggleFunction('${key}')">
                            ${key}: ${functionList[key].enabled ? 'On' : 'Off'}
                        </div>
                    `).join('')}
                </div>
            </div>
        `;

        document.getElementById('close-q-menu').onclick = () => {
            menu.style.display = 'none';
            menuVisible = false;
        };

        // Re-assign toggleFunction handlers without using inline attribute
        const toggles = menu.querySelectorAll('div[onclick]');
        toggles.forEach(div => {
            const matches = div.getAttribute('onclick').match(/toggleFunction\('(.+)'\)/);
            if (!matches) return;
            const key = matches[1];
            div.onclick = () => {
                functionList[key].action();
                updateMenu();
            };
        });
    }

    function isInputFocused() {
        const active = document.activeElement;
        return active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable);
    }

    let menuVisible = false;
    document.addEventListener('keydown', (e) => {
        if ((e.key === 'q' || e.key === 'Q') && !isInputFocused() && !e.repeat) {
            e.preventDefault();
            menuVisible = !menuVisible;
            if (menuVisible) {
                updateMenu();
                menu.style.display = 'block';
            } else {
                menu.style.display = 'none';
            }
        }
    });

    if (state.resize) applyResizeStyle(true);
    if (state.chatFilter) observeChatFilter();
    if (state.avatars) removeAvatars();
    if (state.timestamps) removeTimestamps();
    if (state.itemsUsed) removeUsedItems();
    if (state.ad) toggleAds();
    if (!state.top) toggleVisibility('top-bar_links__4FJwt', false);
    if (!state.left) toggleVisibility('layout_left__O2uku', false);
    if (!state.right) toggleVisibility('chat_chat__2rdNg', false);
    if (!state.stocks) toggleVisibility('stocks-bar_stock__X5bf9 stocks-bar_positive__OQyx5', false);
    if (!state.bottom) toggleVisibility('layout_center-bottom__yhDOH', false);

    // Expose toggleFunction globally for inline onclick handlers
    window.toggleFunction = function(key) {
        if (functionList[key]) {
            functionList[key].action();
            updateMenu();
        }
    };
})();