NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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 ******/
/*********************************************/
})();