Anubys / BBcode Reply

// ==UserScript==
// @name         BBcode Reply
// @namespace    Anubys
// @version      1.0.1
// @description  Ajoute des outils BBCode et quelques raccourcis pratiques sur les forums OGame
// @author       Anubys
// @copyright    2022-2026, Anubys (https://openuserjs.org/users/Anubys)
// @license      MIT
// @updateURL    https://openuserjs.org/meta/Anubys/BBcode_Reply.meta.js
// @downloadURL  https://openuserjs.org/install/Anubys/BBcode_Reply.user.js
// @include      https://board.*.ogame.gameforge.com/*
// @run-at       document-end
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    /***********************************************/
    /*************** Configuration *****************/
    /***********************************************/

    const STORAGE_KEYS = {
        bbcodeStart: 'bbcodeFirst',
        bbcodeEnd: 'bbcodeLast',
        beautifyBefore: 'bbcodeOption1',
        beautifyAfter: 'bbcodeOption2',
        autoSubmit: 'SubmitOption',
        firstLetterOnly: 'FirstLeterOption', // Conservé pour compatibilité avec tes anciennes options
        language: 'bbcodeReplyLanguage',
    };

    const SELECTORS = {
        editor: '#redactor-uuid-0',
        previewButton: '#buttonMessagePreview',
        formSubmit: '.formSubmit',
        primaryButton: '.buttonPrimary',
        messageTabMenu: '[class^="messageTabMenu"]',
        quickReplyContent: '.messageContent.messageQuickReplyContent',
        pageNavigation: '.pageNavigation',
        content: '.content',
        contentTitle: '.contentHeaderTitle > h1',
        notificationButton: '[data-tooltip="Notifications"]',
        notificationDropdownOpen: '.interactiveDropdown.open',
        unreadSubjects: '.tabularListRow > ol.new',
        unreadSections: '.wbbBoardContainer.wbbDepth1.tabularBox .badge.badgeUpdate:not(.badgeUpdateMobile)',
    };

    const state = {
        popupCreated: false,
        isThreadCreationPage: /thread-add/.test(window.location.href),
    };

    const translationsModule = loadTranslationModule();
    const { changeLanguage, getAvailableLanguages, t } = translationsModule;

    const settings = loadSettings();

    /***********************************************/
    /***************** Démarrage *******************/
    /***********************************************/

    init();

    function init() {
        warnIfMissingBbcode();

        initNotificationButton();
        initNavigationClone();
        initReplyTools();
        initSubmitVisibility();
        initUnreadSubjectsButton();
        initUnreadSectionsButton();
        injectCss();
    }

    /***********************************************/
    /************* Lecture des options *************/
    /***********************************************/

    function loadSettings() {
        return {
            bbcodeStart: localStorage.getItem(STORAGE_KEYS.bbcodeStart) || '',
            bbcodeEnd: localStorage.getItem(STORAGE_KEYS.bbcodeEnd) || '',
            beautifyBefore: localStorage.getItem(STORAGE_KEYS.beautifyBefore) === 'true',
            beautifyAfter: localStorage.getItem(STORAGE_KEYS.beautifyAfter) === 'true',
            autoSubmit: localStorage.getItem(STORAGE_KEYS.autoSubmit) === 'true',
            firstLetterOnly: localStorage.getItem(STORAGE_KEYS.firstLetterOnly) === 'true',
        };
    }

    function saveSettings(nextSettings) {
        localStorage.setItem(STORAGE_KEYS.bbcodeStart, nextSettings.bbcodeStart);
        localStorage.setItem(STORAGE_KEYS.bbcodeEnd, nextSettings.bbcodeEnd);
        localStorage.setItem(STORAGE_KEYS.beautifyBefore, String(nextSettings.beautifyBefore));
        localStorage.setItem(STORAGE_KEYS.beautifyAfter, String(nextSettings.beautifyAfter));
        localStorage.setItem(STORAGE_KEYS.autoSubmit, String(nextSettings.autoSubmit));
        localStorage.setItem(STORAGE_KEYS.firstLetterOnly, String(nextSettings.firstLetterOnly));
    }

    function resetSettings() {
        Object.values(STORAGE_KEYS).forEach(key => localStorage.removeItem(key));
        window.location.reload();
    }

    function warnIfMissingBbcode() {
        if (!settings.bbcodeStart || !settings.bbcodeEnd) {
            alert('Entrez votre code dans les options ! Allez dans un sujet et cliquez sur Option.');
        }
    }

    /***********************************************/
    /************ Bouton notifications *************/
    /***********************************************/

    function initNotificationButton() {
        const notificationButton = document.querySelector(SELECTORS.notificationButton);
        if (!notificationButton) return;

        notificationButton.addEventListener('click', () => {
            setTimeout(() => {
                const dropdown = document.querySelector(SELECTORS.notificationDropdownOpen);
                if (!dropdown || dropdown.querySelector('.buttonNotif')) return;

                const openAllButton = document.createElement('button');
                openAllButton.type = 'button';
                openAllButton.className = 'buttonNotif';
                openAllButton.title = 'Ouvre toutes les notifications non lues';
                openAllButton.addEventListener('click', openNotifications);

                dropdown.prepend(openAllButton);
            }, 1);
        });
    }

    function openNotifications() {
        const unreadNotifications = document.querySelectorAll(
            '.notificationItem.interactiveDropdownItemOutstanding.interactiveDropdownItemOutstandingIcon.interactiveDropdownItemShadow'
        );

        unreadNotifications.forEach(notification => {
            const links = notification.querySelectorAll('div > div > h3 a');
            const lastLink = links[links.length - 1];
            if (lastLink) window.open(lastLink.href, '_blank');
        });
    }

    /***********************************************/
    /*********** Barre de navigation bas ***********/
    /***********************************************/

    function initNavigationClone() {
        const navigation = document.querySelector(SELECTORS.pageNavigation);
        const container = document.querySelector(SELECTORS.content);
        if (!navigation || !container || document.querySelector('#PageNavigationClone')) return;

        const clone = navigation.cloneNode(true);
        clone.id = 'PageNavigationClone';
        clone.style.display = 'block ruby';
        clone.style.marginTop = '10px';
        clone.style.background = 'rgba(127, 159, 180, 0.085) none repeat scroll 0 0';

        container.appendChild(clone);
    }

    /***********************************************/
    /************* Outils de réponse ***************/
    /***********************************************/

    function initReplyTools() {
        if (!document.querySelector(SELECTORS.previewButton)) return;

        initBeautifyButtons();
        initOptionsButton();
        initCloseWindowButton();
    }

    function initBeautifyButtons() {
        const submitBars = document.querySelectorAll(SELECTORS.formSubmit);
        const lastSubmitBar = submitBars[submitBars.length - 1];
        if (!lastSubmitBar) return;

        if (settings.beautifyBefore) {
            const beforeButton = createButton({
                text: t('beautify'),
                className: 'button',
                onClick: insertBbcodeAtEnd,
            });

            applyBbcodeColorToButton(beforeButton);
            lastSubmitBar.prepend(beforeButton);
        }

        if (settings.beautifyAfter) {
            const afterButton = createButton({
                text: t('beautify'),
                className: 'button',
                id: 'buttonBeautify',
                onClick: settings.firstLetterOnly ? decorateFirstLetterAndSubmit : decorateEveryLineAndSubmit,
            });

            applyBbcodeColorToButton(afterButton);
            lastSubmitBar.prepend(afterButton);
        }
    }

    function initOptionsButton() {
        const messageTabMenu = document.querySelector(SELECTORS.messageTabMenu);
        const list = messageTabMenu?.querySelector('ul');
        if (!list || list.querySelector('.buttonOption')) return;

        const optionButton = createButton({
            text: 'Option',
            className: 'buttonOption',
            onClick: openOptionsPopup,
        });

        list.appendChild(optionButton);
    }

    function initCloseWindowButton() {
        if (state.isThreadCreationPage) return;

        const quickReply = document.querySelector(SELECTORS.quickReplyContent);
        if (!quickReply || quickReply.parentNode.querySelector('.buttonCloseWindow')) return;

        const closeButton = createButton({
            className: 'buttonCloseWindow',
            title: 'Ferme cet onglet',
            onClick: () => window.close(),
        });

        quickReply.parentNode.prepend(closeButton);
    }

    function createButton({ text = '', className = '', id = '', title = '', onClick }) {
        const button = document.createElement('button');
        button.type = 'button';
        button.textContent = text;
        button.className = className;
        button.title = title;

        if (id) button.id = id;
        if (onClick) button.addEventListener('click', onClick);

        return button;
    }

    function applyBbcodeColorToButton(button) {
        const colorMatch = /#[a-z0-9]{6}/i.exec(settings.bbcodeStart);
        button.style.background = colorMatch ? colorMatch[0] : '#ff8c00';
    }

    /***********************************************/
    /*************** Submit / autosubmit ***********/
    /***********************************************/

    function initSubmitVisibility() {
        if (!settings.autoSubmit) return;

        const submitButton = getSubmitButton();
        if (submitButton) submitButton.style.display = 'none';
    }

    function autoSubmit() {
        if (!settings.autoSubmit) return;

        const submitButton = getSubmitButton();
        if (!submitButton) return;

        if (state.isThreadCreationPage) {
            setTimeout(() => submitButton.click(), 500);
        } else {
            submitButton.click();
        }
    }

    function getSubmitButton() {
        if (state.isThreadCreationPage) {
            return document.querySelector(`${SELECTORS.formSubmit} input`);
        }

        const primaryButtons = document.querySelectorAll(SELECTORS.primaryButton);
        return primaryButtons[1] || primaryButtons[0] || null;
    }

    /***********************************************/
    /************* Insertion du BBCode *************/
    /***********************************************/

    function getEditor() {
        return document.querySelector(SELECTORS.editor);
    }

    function insertBbcodeAtEnd() {
        const editor = getEditor();
        if (!editor) return;

        const paragraphs = editor.querySelectorAll('p');
        const lastParagraph = paragraphs[paragraphs.length - 1];
        if (!lastParagraph) return;

        lastParagraph.appendChild(document.createTextNode(`${settings.bbcodeStart}  ${settings.bbcodeEnd}`));
    }

    function decorateEveryLineAndSubmit() {
        const editor = getEditor();
        if (!editor) return;

        const editableItems = editor.querySelectorAll(':scope > p, :scope > ul > li, :scope > ol > li');

        editableItems.forEach(item => {
            item.prepend(document.createTextNode(settings.bbcodeStart));
            item.append(document.createTextNode(settings.bbcodeEnd));
        });

        autoSubmit();
    }

    function decorateFirstLetterAndSubmit() {
        const editor = getEditor();
        if (!editor) return;

        const editableItems = editor.querySelectorAll(':scope > p, :scope > ul > li, :scope > ol > li');

        editableItems.forEach(item => {
            decorateFirstTextCharacter(item, settings.bbcodeStart, settings.bbcodeEnd);
        });

        autoSubmit();
    }

    function decorateFirstTextCharacter(rootElement, before, after) {
        const textNode = findFirstUsefulTextNode(rootElement);
        if (!textNode) return;

        const characterIndex = findFirstNonSpaceIndex(textNode.textContent);
        if (characterIndex === -1) return;

        const text = textNode.textContent;
        const firstPart = text.slice(0, characterIndex);
        const character = text[characterIndex];
        const lastPart = text.slice(characterIndex + 1);

        const fragment = document.createDocumentFragment();

        if (firstPart) {
            fragment.appendChild(document.createTextNode(firstPart));
        }

        fragment.appendChild(document.createTextNode(before));
        fragment.appendChild(document.createTextNode(character));
        fragment.appendChild(document.createTextNode(after));

        if (lastPart) {
            fragment.appendChild(document.createTextNode(lastPart));
        }

        textNode.parentNode.replaceChild(fragment, textNode);
    }

    function findFirstUsefulTextNode(rootElement) {
        const walker = document.createTreeWalker(
            rootElement,
            NodeFilter.SHOW_TEXT,
            {
                acceptNode(node) {
                    return findFirstNonSpaceIndex(node.textContent) === -1
                        ? NodeFilter.FILTER_REJECT
                        : NodeFilter.FILTER_ACCEPT;
                },
            }
        );

        return walker.nextNode();
    }

    function findFirstNonSpaceIndex(text) {
        for (let index = 0; index < text.length; index += 1) {
            if (text[index].trim() !== '') return index;
        }

        return -1;
    }

    /***********************************************/
    /*************** Boutons lecture ***************/
    /***********************************************/

    function initUnreadSubjectsButton() {
        const unreadSubjects = document.querySelectorAll(SELECTORS.unreadSubjects);
        if (unreadSubjects.length === 0) return;

        const title = document.querySelector(SELECTORS.contentTitle);
        if (!title) return;

        const openButton = createButton({
            className: 'NewLecture',
            title: 'Ouvre les sujets non lus',
            onClick: () => {
                unreadSubjects.forEach(subject => {
                    const link = subject.querySelector('.messageGroupLink');
                    if (link) window.open(link.href, '_blank');
                });
            },
        });

        title.appendChild(openButton);
    }

    function initUnreadSectionsButton() {
        const unreadSections = document.querySelectorAll(SELECTORS.unreadSections);
        if (unreadSections.length === 0) return;

        const title = document.querySelector(SELECTORS.contentTitle);
        if (!title) return;

        const openButton = createButton({
            className: 'NewLecture',
            title: 'Ouvre les sections avec messages non lus',
            onClick: () => {
                unreadSections.forEach(section => {
                    const link = section.parentElement?.querySelector('a');
                    if (link) window.open(link.href, '_blank');
                });
            },
        });

        title.appendChild(openButton);
    }

    /***********************************************/
    /**************** Popup options ****************/
    /***********************************************/

    function openOptionsPopup() {
        const existingPopup = document.querySelector('.popup');

        if (existingPopup) {
            existingPopup.style.display = 'block';
            return;
        }

        createOptionsPopup();
        state.popupCreated = true;
    }

    function createOptionsPopup() {
        const popup = document.createElement('div');
        popup.className = 'popup';

        const titleRow = document.createElement('div');
        titleRow.className = 'Titre';
        titleRow.textContent = 'Options';

        const closeButton = createButton({
            id: 'buttonExit',
            title: 'Fermer les options',
            onClick: () => {
                popup.style.display = 'none';
            },
        });

        titleRow.appendChild(closeButton);
        popup.appendChild(titleRow);

        const langSelect = createLanguageSelect();
        const bbcodeStartInput = createInput('Sitting1', settings.bbcodeStart);
        const bbcodeEndInput = createInput('Sitting2', settings.bbcodeEnd);
        const beforeCheckbox = createCheckbox('checkboxOption1', settings.beautifyBefore);
        const afterCheckbox = createCheckbox('checkboxOption2', settings.beautifyAfter);
        const autoSubmitCheckbox = createCheckbox('checkboxOption3', settings.autoSubmit);
        const firstLetterCheckbox = createCheckbox('checkboxOption4', settings.firstLetterOnly);

        popup.appendChild(createOptionRow(`${t('lang')} :`, langSelect));
        popup.appendChild(createOptionRow(`${t('beginning_bbcode')} :`, bbcodeStartInput));
        popup.appendChild(createOptionRow(`${t('bbcode_end')} :`, bbcodeEndInput));
        popup.appendChild(createOptionRow(`${t('beautify_before')} :`, beforeCheckbox));
        popup.appendChild(createOptionRow(`${t('beautify_after')} :`, afterCheckbox));
        popup.appendChild(createOptionRow('Première lettre seulement :', firstLetterCheckbox));
        popup.appendChild(createOptionRow(`${t('automatically_post')} :`, autoSubmitCheckbox));

        const resetButton = createButton({
            text: 'Reset',
            className: 'Reset',
            onClick: resetSettings,
        });

        popup.appendChild(createOptionRow(`${t('RAZ')} :`, resetButton));

        const footer = document.createElement('div');
        footer.className = 'popupFooter';

        const validButton = createButton({
            text: 'Valider',
            id: 'buttonValidation',
            onClick: () => {
                changeLanguage(langSelect.value);

                saveSettings({
                    bbcodeStart: bbcodeStartInput.value,
                    bbcodeEnd: bbcodeEndInput.value,
                    beautifyBefore: beforeCheckbox.checked,
                    beautifyAfter: afterCheckbox.checked,
                    autoSubmit: autoSubmitCheckbox.checked,
                    firstLetterOnly: firstLetterCheckbox.checked,
                });

                window.location.reload();
            },
        });

        footer.appendChild(validButton);
        popup.appendChild(footer);

        document.body.appendChild(popup);
    }

    function createOptionRow(labelText, inputElement) {
        const row = document.createElement('div');
        row.className = 'optionRow';

        const label = document.createElement('label');
        label.textContent = labelText;

        row.appendChild(label);
        row.appendChild(inputElement);

        return row;
    }

    function createInput(id, value) {
        const input = document.createElement('input');
        input.className = 'Input';
        input.id = id;
        input.value = value || '';
        return input;
    }

    function createCheckbox(id, checked) {
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.id = id;
        checkbox.checked = checked;
        return checkbox;
    }

    function createLanguageSelect() {
        const currentLanguage = changeLanguage();
        const select = document.createElement('select');
        select.id = 'bbcodeReplyLangSelect';
        select.className = 'Input';

        getAvailableLanguages().forEach(language => {
            const option = document.createElement('option');
            option.value = language;
            option.textContent = language;
            option.selected = language === currentLanguage;
            select.appendChild(option);
        });

        return select;
    }

    /***********************************************/
    /******************* CSS ***********************/
    /***********************************************/

    function injectCss() {
        if (document.querySelector('#bbcodeReplyStyle')) return;

        const style = document.createElement('style');
        style.id = 'bbcodeReplyStyle';
        style.type = 'text/css';
        style.textContent = `
.Titre {
    position: relative;
    text-align: center;
    color: orange;
    font-size: large;
    padding: 8px 36px 8px 8px;
}

.button {
    padding: 8px 10px;
    font-size: 14px;
}

.buttonOption {
    padding: 1px 10px;
    font-size: 10px;
    background-color: rgba(19, 18, 19, 0.78);
    margin: 2px;
}

.popup {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 99999;
    width: 380px;
    max-width: calc(100vw - 30px);
    height: auto;
    background-color: rgba(19, 18, 19, 0.92);
    border-radius: 15px;
    box-shadow: 0 10px 35px rgba(0, 0, 0, 0.45);
    color: inherit;
}

.optionRow {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    min-height: 34px;
    padding: 8px 14px;
}

.optionRow label {
    flex: 1;
}

.Input {
    width: 150px;
    max-width: 55%;
}

.Reset {
    margin: 2px;
    padding: 1px 10px;
    font-size: 10px;
    background: #FF0000;
}

.popupFooter {
    display: flex;
    justify-content: center;
    padding: 12px;
}

.buttonCloseWindow {
    position: absolute;
    width: 20px;
    height: 20px;
    background: rgba(255, 50, 50, 1);
    margin-left: 212px;
    margin-top: 8px;
    border-radius: 300px;
    z-index: 1;
    padding: 0;
    box-shadow: -2px -2px 5px rgba(0, 0, 0, 0.5) inset, 1px 1px 8px rgba(0, 0, 0, 0.5);
}

.buttonCloseWindow:hover {
    background: rgba(50, 255, 50, 1);
}

.NewLecture {
    position: relative;
    display: inline-block;
    width: 20px;
    height: 20px;
    background: rgba(244, 102, 27, 1);
    border-radius: 300px;
    padding: 0;
    margin-top: 7px;
    margin-left: 20px;
    box-shadow: -2px -2px 5px rgba(0, 0, 0, 0.5) inset, 1px 1px 8px rgba(0, 0, 0, 0.5);
    vertical-align: middle;
}

.NewLecture:hover {
    background: rgba(50, 255, 50, 1);
}

.buttonNotif {
    position: absolute;
    width: 20px;
    height: 20px;
    margin-top: 1em;
    margin-left: 10em;
    background: yellow;
    border-radius: 300px;
    padding: 0 !important;
    box-shadow: -2px -2px 5px rgba(0, 0, 0, 0.5) inset, 1px 1px 8px rgba(0, 0, 0, 0.5);
}

.buttonNotif:hover {
    background: rgba(50, 255, 50, 1);
}

#PageNavigationClone .breadcrumbs {
    margin-bottom: 0;
    z-index: 0;
}

#PageNavigationClone.pageNavigation .pageNavigationIcons {
    display: none;
}

section#main #content {
    padding-top: 30px;
}

.pageNavigation .breadcrumbs {
    margin-bottom: -80px;
}

#buttonValidation {
    width: 70px;
    height: 30px;
    padding: 1px 10px;
    font-size: 10px;
    background: #94e733;
    margin: 2px;
    border-radius: 15px;
}

#buttonExit {
    position: absolute;
    top: 7px;
    right: 8px;
    width: 22px;
    height: 22px;
    padding: 0;
    border-radius: 10px;
    background: #FF0000;
}
`;

        document.head.appendChild(style);
    }

    /***********************************************/
    /******** Gestion des traductions - début ******/
    /***********************************************/

    function loadTranslationModule() {
        const translations = {
            fr: {
                lang: 'Langue',
                beautify: 'Décorer',
                beginning_bbcode: 'Début de votre BBCode',
                bbcode_end: 'Fin de votre BBCode',
                beautify_before: 'Décorer avant',
                beautify_after: 'Décorer après',
                RAZ: 'Remise à zéro',
                automatically_post: 'Poster automatiquement',
            },

            en: {
                lang: 'Language',
                beautify: 'Beautify',
                beginning_bbcode: 'Opening BBCode',
                bbcode_end: 'Closing BBCode',
                beautify_before: 'Beautify before',
                beautify_after: 'Beautify after',
                RAZ: 'Reset',
                automatically_post: 'Post automatically',
            },

            de: {
                lang: 'Sprache',
                beautify: 'Verschönern',
                beginning_bbcode: 'Öffnender BBCode',
                bbcode_end: 'Schließender BBCode',
                beautify_before: 'Vorher verschönern',
                beautify_after: 'Nachher verschönern',
                RAZ: 'Zurücksetzen',
                automatically_post: 'Automatisch posten',
            },
        };

        const mainLanguage = 'fr';
        let currentLanguage = localStorage.getItem(STORAGE_KEYS.language) || mainLanguage;

        if (!translations[currentLanguage]) {
            currentLanguage = mainLanguage;
        }

        checkTranslationConsistency();

        function t(translationKey) {
            return translations[currentLanguage]?.[translationKey] || translations[mainLanguage][translationKey] || translationKey;
        }

        function changeLanguage(newLanguage) {
            if (newLanguage && translations[newLanguage]) {
                currentLanguage = newLanguage;
                localStorage.setItem(STORAGE_KEYS.language, newLanguage);
            }

            return currentLanguage;
        }

        function getAvailableLanguages() {
            return Object.keys(translations);
        }

        // Vérifie que chaque langue possède les mêmes clés que la langue principale.
        function checkTranslationConsistency() {
            const mainKeys = Object.keys(translations[mainLanguage]);

            Object.entries(translations).forEach(([language, languageTranslations]) => {
                const missingKeys = mainKeys.filter(key => !(key in languageTranslations));

                if (missingKeys.length > 0) {
                    console.warn(
                        `[BBcode Reply] Traduction incomplète pour "${language}" : ${missingKeys.join(', ')}`
                    );
                }
            });
        }

        return {
            changeLanguage,
            getAvailableLanguages,
            t,
        };
    }

    /*********************************************/
    /******** Gestion des traductions - fin ******/
    /*********************************************/
})();