Greasy Fork is available in English.

Camamba Chat Tweaks

tweaks layout of the chat

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name            Camamba Chat Tweaks
// @namespace       dannysaurus.camamba
// @version         0.5.14
// @description     tweaks layout of the chat
// @license         MIT License
//
// @include         https://www.camamba.com/chat/
// @include         https://www.de.camamba.com/chat/
//
// @connect         camamba.com
// @grant           GM_xmlhttpRequest
//
// @require         https://greasyfork.org/scripts/405143-simplecache/code/SimpleCache.js
// @require         https://greasyfork.org/scripts/405144-httprequest/code/HttpRequest.js?version=1106047
// @require         https://greasyfork.org/scripts/391854-enum/code/Enum.js
// @require         https://greasyfork.org/scripts/405699-camamba-user/code/Camamba%20User.js
//
// @require         https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
//
// @require         https://greasyfork.org/scripts/423722-camamba-chat-helpers-library/code/Camamba%20Chat%20Helpers%20Library.js?version=960246
// @require         https://greasyfork.org/scripts/423662-camamba-chat-settings/code/Camamba%20Chat%20Settings.js?version=913122
// @require         https://greasyfork.org/scripts/423665-camamba-hook-into-onmessage/code/Camamba%20Hook%20Into%20OnMessage.js?version=1180072
//
// @grant           GM.getValue
// @grant           GM.setValue
// ==/UserScript==

// https://greasyfork.org/de/scripts/419077-camamba-chat-tweaks

/* jslint esnext: true */
/* globals knownUsers, me */
(function () {
    'use strict';
    // --- initial sizes ---
    const SIZES = {
        FONT_EM: {
            userList: 1.2,
            chatBox: 1.8,
        },
        WIDTH_EM: {
            sidebarLeft: 10,
            sidebarRight: 14,
        },
    };

    // --- HTML Selector Helpers ---
    const SELECTORS = {
        ID: {
            // original
            userList: 'userList',
            chatBox: 'chatBox',
            chatInput: 'chatInput',
            chatWindow: 'chatWindow',
            mediaContainer1: 'mediaContainer1',
            mediaContainer2: 'mediaContainer2',
            mediaContainer3: 'mediaContainer3',
            mediaContainer4: 'mediaContainer4',
            mediaContainer5: 'mediaContainer5',
            mediaContainer6: 'mediaContainer6',
            mediaContainer7: 'mediaContainer7',
            mediaContainer8: 'mediaContainer8',

            // script
            cbCamslots: 'cb-camslots',
            spinnerUserlistFont: 'spinner-userlist-font',
            spinnerChatFont: 'spinner-chat-font',
            unamePermaInput: 'uname-perma-input',
            cbPrivateConvo: 'cb-privateConvo',
        },
        CLASS: {
            noTextSelect: 'noTextSelect',
            borderBox: 'borderBox',
            camBox: 'camBox'
        }
    };

    const containers = (() => {
        let userList, chatBox, sidebars, camslots;

        return {
            get userList() {
                if (typeof userList === "undefined") {
                    userList = document.getElementById(SELECTORS.ID.userList);
                }
                return userList;
            },

            get chatBox() {
                if (typeof chatBox === "undefined") {
                    chatBox = document.getElementById(SELECTORS.ID.chatBox);
                }
                return chatBox;
            },

            get sidebars() {
                if (typeof sidebars === "undefined") {
                    sidebars = document.getElementById(SELECTORS.ID.chatWindow).querySelectorAll(`.${SELECTORS.CLASS.noTextSelect}`);
                }
                return sidebars;
            },

            get sidebarLeft() {
                return this.sidebars[0];
            },

            get sidebarTop() {
                return this.sidebars[1];
            },

            get sidebarRight() {
                return this.sidebars[2];
            },

            get camslots() {
                if (typeof camslots === "undefined") {
                    const parentContainers = [
                        SELECTORS.ID.mediaContainer1,
                        SELECTORS.ID.mediaContainer2,
                        SELECTORS.ID.mediaContainer3,
                        SELECTORS.ID.mediaContainer4,
                        SELECTORS.ID.mediaContainer5,
                        SELECTORS.ID.mediaContainer6,
                        SELECTORS.ID.mediaContainer7,
                        SELECTORS.ID.mediaContainer8,
                    ]
                        .map(id => document.getElementById(id))
                        .filter(el => el !== null)
                        .map(el => el.parentNode);

                    camslots = [...new Set(parentContainers)];
                }
                return camslots;
            }
        };
    })();

    const layoutPatcher = new class {
        constructor() {
            this.historyCamslotsRemoved = [];
        }

        patchSizes() {
            // this.setWidthOfSidebarLeft(`${SIZES.WIDTH_EM.sidebarLeft}em`);
            this.setWidthOfSidebarRight(`${SIZES.WIDTH_EM.sidebarRight}em`);
            return this;
        }

        setFontSizeOfUserList(fontSize) {
            containers.userList.style.fontSize = fontSize;
            return this;
        }

        setFontSizeOfChat(fontSize) {
            containers.chatBox.style.fontSize = fontSize;
            return this;
        }

        setWidthOfSidebarLeft(width) {
            containers.sidebarLeft.style.width = width;
            return this;
        }

        setWidthOfSidebarRight(width) {
            containers.sidebarLeft.style.width = width;
            return this;
        }

        showCamslots() {
            for (let i = 0; i < this.historyCamslotsRemoved.length; i++) {
                const { parent, index, element } = this.historyCamslotsRemoved.pop();
                parent.insertBefore(element, parent.children[index]);
            }
            return this;
        }

        hideCamslots() {
            for (let element of containers.camslots) {
                const parent = element.parentNode;
                if (parent) {
                    let index = Array.from(parent.children).indexOf(element);
                    parent.removeChild(element);

                    this.historyCamslotsRemoved.push({ parent, index, element });
                }
            }
            return this;
        }
    }();


    const controls = (() => {
        // --- HTML Create Element Helpers ---
        const createInput = ({
            id,
            parentElement = null,
            type = 'text',
            defaultValue = '',
            labelText = null,
            onValueChange = null,
            propertyNameValue = 'value',
            eventNameValueChange = 'input',
        }) => {
            const div = document.createElement('div');

            const input = div.appendChild(document.createElement('input'));
            input.type = type;
            input.id = id;
            input.style.backgroundColor = 'rgba(39,62,77,1)';

            if (labelText) {
                const label = div.appendChild(document.createElement('label'));
                label.htmlFor = id;
                label.appendChild(document.createTextNode(labelText));
            }

            if (onValueChange) {
                let oldValue;

                input.addEventListener(eventNameValueChange, () => {
                    const newValue = input[propertyNameValue];
                    if (oldValue !== newValue) {
                        oldValue = newValue;

                        onValueChange(newValue);
                    }
                });
            }

            if (parentElement) {
                parentElement.appendChild(div);
            }
            return input;
        };

        const createInputPersistent = ({
            id,
            parentElement = null,
            type = 'text',
            defaultValue = '',
            labelText = null,
            onValueChange = null,
            propertyNameValue = 'value',
            eventNameValueChange = 'input',
        }) => {
            const input = createInput({
                parentElement, type, id, defaultValue, labelText, propertyNameValue, eventNameValueChange,
                onValueChange: value => {
                    GM.setValue(id, value);
                    if (onValueChange) {
                        onValueChange(value);
                    }
                }
            });

            input.setValue = value => {
                GM.setValue(id, value);
                input[propertyNameValue] = value;
                onValueChange(value);
            };

            input.updateValue = () => GM.getValue(id, defaultValue).then(value => {
                input[propertyNameValue] = value;
                if (onValueChange) {
                    onValueChange(value);
                }
            });

            return input;
        };

        const createCheckbox = ({
            id,
            parentElement = null,
            initialChecked = false,
            labelText = null,
            onValueChange = null,
        }) => {
            const checkbox = createInputPersistent({
                parentElement, id, labelText, onValueChange,
                defaultValue: !!initialChecked,
                type: 'checkbox',
                propertyNameValue: 'checked',
                eventNameValueChange: 'click',
            });
            return checkbox;
        };

        const createSpinner = ({
            id, min, max, step,
            parentElement = null,
            defaultValue = 0,
            labelText = null,
            onValueChange = null,
        }) => {
            const spinner = createInputPersistent({
                parentElement, id, defaultValue, labelText, onValueChange,
                type: 'number',
            });
            spinner.min = min;
            spinner.max = max;
            spinner.step = step;

            const buttonDec = spinner.parentNode.insertBefore(document.createElement('button'), spinner);
            buttonDec.type = 'button';
            buttonDec.innerHTML = '-';
            buttonDec.addEventListener('click', () => {
                spinner.stepDown();
                spinner.setValue(spinner.value);
            });

            const buttonInc = spinner.parentNode.insertBefore(document.createElement('button'), spinner.nextSibling);
            buttonInc.type = 'button';
            buttonInc.innerHTML = '+';
            buttonInc.addEventListener('click', () => {
                spinner.stepUp();
                spinner.setValue(spinner.value);
            });

            return spinner;
        };

        const sidebarLeftCenter = containers.sidebarLeft.children[1];
        sidebarLeftCenter.innerHTML = "";
        const container = sidebarLeftCenter.appendChild(document.createElement('div'));

        // checkbox camslots on/off
        const cbCamslots = createCheckbox({
            parentElement: container,
            id: SELECTORS.ID.cbCamslots,
            initialChecked: true,
            labelText: 'camslots',
            onValueChange: value => {
                if (value) {
                    layoutPatcher.showCamslots();
                } else {
                    layoutPatcher.hideCamslots();
                }
            },
        });

        // spinner userlist font
        const spinnerUserlistFont = createSpinner({
            parentElement: container,
            id: SELECTORS.ID.spinnerUserlistFont,
            defaultValue: SIZES.FONT_EM.userList,
            min: 1.0,
            max: 3.2,
            step: 0.1,
            labelText: 'users',
            onValueChange: value => {
                const fontSize = `${value}em`;
                layoutPatcher.setFontSizeOfUserList(fontSize);
            },
        });

        // spinner chat font
        const spinnerChatFont = createSpinner({
            parentElement: container,
            id: SELECTORS.ID.spinnerChatFont,
            defaultValue: SIZES.FONT_EM.chatBox,
            min: 1.0,
            max: 5.5,
            step: 0.1,
            labelText: 'chat',
            onValueChange: value => {
                const fontSize = `${value}em`;
                layoutPatcher.setFontSizeOfChat(fontSize);
            },
        });

        const buttonKickFromCam = container.appendChild(document.createElement('button'));
        buttonKickFromCam.type = 'button';
        buttonKickFromCam.innerHTML = 'Kick from cam';
        buttonKickFromCam.addEventListener('click', () => {
            knownUsers.bySelected().stopViewing();
        });

        if (me.admin) {
            const labelUnamePerma = container.appendChild(document.createElement('label'));
            labelUnamePerma.type = 'text';
            labelUnamePerma.for = "uname-perma";
            labelUnamePerma.innerHTML = 'Username Perma';

            const inputUnamePerma = container.appendChild(document.createElement('input'));
            inputUnamePerma.type = 'text';
            inputUnamePerma.id = SELECTORS.ID.unamePermaInput;
            inputUnamePerma.name = 'uname-perma';

            const buttonPerma = container.appendChild(document.createElement('button'));
            buttonPerma.type = 'button';
            buttonPerma.innerHTML = 'perma';
            buttonPerma.addEventListener('click', () => {
                const unamePerma = document.getElementById(SELECTORS.ID.unamePermaInput).value;
                if (unamePerma) {
                    knownUsers.addExact(unamePerma).then(() => knownUsers.byName(unamePerma).banPermaFast(""));
                } else {
                    knownUsers.bySelected().ban("You are permanently banned from Camamba. Please do not create any additional accounts!", 24, { isPublic: true, isPerma: true, suppressBanLog: false });
                }
            });
        }

        const isGerman = location.hostname === "www.de.camamba.com";


        let oldPrivateHandler = null;

        // checkbox camslots on/off
        const cbPrivateConvo = createCheckbox({
            parentElement: container,
            id: SELECTORS.ID.cbPrivateConvo,
            initialChecked: true,
            labelText: isGerman ? 'PN ablehnen ohne Freundschaft' : 'PM denie withouth friendship',
            onValueChange: (value) => {
                if (value) {
                    if (!oldPrivateHandler && onMessageHandlers.private) {
                        oldPrivateHandler = onMessageHandlers.private;
                        console.log("Alter Handler gesichert.", oldPrivateHandler.toString());
                    }
                    /** 
                     * @param {{ id: number }} data  
                     * @return {boolean} - true if further handling is required, false if action is fully handled
                     */
                    onMessageHandlers.private = (data) => {
                        if (!data.id) {
                            return true;
                        }
                        const user = knownUsers[data.id];
                        if (!user) {
                            console.log(`Unknown user with id ${data.id} requesting ${"privConvo"}.`);
                            return false;
                        }

                        if (!user.friend) {
                            wsSend({ command: "control", target: data.id, request: "privReject" });
                            console.log(`PN von ${user.name} abgelehnt`)
                            return false;
                        }
                        console.log(`PN von ${user.name} erlaubt`)
                        console.log("Alter Handler ausgeführt.")
                        if (typeof oldPrivateHandler === 'function') {
                            return oldPrivateHandler(data);
                        }
                        return true;
                    };
                } else {
                    if (oldPrivateHandler) {
                        onMessageHandlers.private = oldPrivateHandler;
                        console.log("Alter Handler wiederhergestellt.")
                    }
                }
            },
        });

        return {
            cbCamslots,
            spinnerUserlistFont,
            spinnerChatFont,
            cbPrivateConvo,
        };
    })();

    const wait = async (ms) => new Promise(res => setTimeout(res, ms));
    (async () => {
        // wait until websocket has been connected
        while (typeof initSettings !== 'function') {
            await wait(100);
        }

        const original = initSettings;
        initSettings = () => {
            original();

            // Breite von Userliste anpassen
            layoutPatcher.patchSizes();

            // weiterere Einstellungen überschreiben, bzw übernehmen
            for (let control of [controls.cbCamslots, controls.spinnerUserlistFont, controls.spinnerChatFont, controls.cbPrivateConvo]) {
                control.updateValue();
            }
        };
    })();

    (async () => {
        let lastBanData = { userId: 0, text: '', time: 0, isPerma: false };

        while (typeof adminExec !== 'function') {
            await wait(100);
        }

        adminExec();

        if (currentAdminAction == "ban") {
            let userId, text, time, isPerma;

            text = byId('adminMessageInput').value;
            if (!text || text.length <= 3 && byId('adminMessageSelect').selectedIndex) {
                text = adminMessages[currentAdminAction][byId('adminMessageSelect').value];
            }

            userId = currentAdminTarget;
            time = parseInt(byId('banTime').value);
            isPerma = byId('permaBan') && byId('permaBan').checked;

            if (userId && text > 3 && time) {
                lastBanData = { userId, text, time, isPerma };
            }
        }
    })();


    (async () => {
        while (document.getElementById(SELECTORS.ID.chatInput) === null) {
            await wait(100);
        }
        document.getElementById(SELECTORS.ID.chatInput).setAttribute('autoComplete', 'on');
    })();

    console.log("running camamba chat tweaks")
})();