Menu de opções avançadas para melhorar a visibilidade dos recursos no mapa do DofusDB. Disponível em todos os idiomas.
// ==UserScript==
// @name DofusDB Map Advanced Options UI
// @name:fr DofusDB Map Advanced Options UI
// @name:en DofusDB Map Advanced Options UI
// @name:de DofusDB Karten-Erweiterte Optionen UI
// @name:es Interfaz de opciones avanzadas del mapa DofusDB
// @name:pt-BR Interface de opções avançadas do mapa DofusDB
// @description Menu d'options avancées pour améliorer la visibilité des ressources sur la map du site DofusDB. Disponible dans toutes les langues.
// @description:fr Menu d'options avancées pour améliorer la visibilité des ressources sur la map du site DofusDB. Disponible dans toutes les langues.
// @description:en Advanced options menu to improve resource visibility on the DofusDB map. Available in all languages.
// @description:de Erweiterte Optionen zur Verbesserung der Ressourcensichtbarkeit auf der DofusDB-Karte. In allen Sprachen verfügbar.
// @description:es Menú de opciones avanzadas para mejorar la visibilidad de los recursos en el mapa de DofusDB. Disponible en todos los idiomas.
// @description:pt-BR Menu de opções avançadas para melhorar a visibilidade dos recursos no mapa do DofusDB. Disponível em todos os idiomas.
// @namespace http://tampermonkey.net/
// @version 1.0.2
// @author Haorow
// @homepageURL https://greasyfork.org/scripts/576074
// @supportURL https://greasyfork.org/scripts/576074-dofusdb-map-advanced-options-ui/feedback
// @match https://dofusdb.fr/*
// @grant none
// @license MIT
// ==/UserScript==
/* eslint-disable no-multi-spaces */
(function () {
'use strict';
// ========================
// TRANSLATIONS
// ========================
const TRANSLATIONS = {
fr: { options: 'Options', enable: 'Activer les options', theme: 'Thème', opacity: 'Opacité de la carte', scale: 'Taille des ressources', border: 'Bord des ressources' },
en: { options: 'Options', enable: 'Enable options', theme: 'Theme', opacity: 'Map opacity', scale: 'Resource size', border: 'Resource border' },
de: { options: 'Optionen', enable: 'Optionen aktivieren', theme: 'Thema', opacity: 'Karten-Deckkraft', scale: 'Ressourcengröße', border: 'Ressourcenrand' },
es: { options: 'Opciones', enable: 'Activar opciones', theme: 'Tema', opacity: 'Opacidad del mapa', scale: 'Tamaño de recursos', border: 'Borde de recursos' },
pt: { options: 'Opções', enable: 'Ativar opções', theme: 'Tema', opacity: 'Opacidade do mapa', scale: 'Tamanho dos recursos', border: 'Borda dos recursos' }
};
function getTranslations() {
const m = location.pathname.match(/^\/(fr|en|de|es|pt)\//);
return TRANSLATIONS[m?.[1]] || TRANSLATIONS.fr;
}
// ========================
// STATE
// ========================
const state = {
enabled: localStorage.getItem('mapEnabled') !== 'false',
opacity: parseFloat(localStorage.getItem('mapOpacity')) || 0.65,
scale: parseFloat(localStorage.getItem('mapScale')) || 1.15,
isLight: localStorage.getItem('mapTheme') !== 'dark',
showBorders: localStorage.getItem('mapBorders') === 'true'
};
// ========================
// STYLES
// ========================
const dynamicStyle = document.head.appendChild(document.createElement('style'));
const staticStyle = document.head.appendChild(document.createElement('style'));
staticStyle.textContent = `
.leaflet-container { transition: background 0.3s ease; }
.leaflet-tile-pane .leaflet-layer { transition: opacity 0.2s ease; }
.resources-marker-wrapper { transition: transform 0.2s ease; }
#map-options-menu input[type="range"] {
-webkit-appearance: none;
width: 140px; height: 6px;
border-radius: 999px;
outline: none;
background: linear-gradient(to right, #00c853 0%, #00c853 var(--value, 50%), #ddd var(--value, 50%), #ddd 100%);
}
#map-options-menu input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px; height: 14px;
border-radius: 50%;
background: #00c853;
cursor: pointer;
}
#map-options-menu input[type="range"]::-moz-range-thumb {
width: 14px; height: 14px;
border-radius: 50%;
background: #00c853;
cursor: pointer;
}
`;
function applyStyle() {
document.head.appendChild(dynamicStyle); // garantit la priorité face aux CSS chargés en lazy par DofusDB
if (!state.enabled) {
dynamicStyle.textContent = '';
return;
}
const { isLight, opacity, scale, showBorders } = state;
const rgb = isLight ? '0,0,0' : '255,255,255';
dynamicStyle.textContent = `
.leaflet-container { background: ${isLight ? '#ffffff' : '#272727'} !important; }
.leaflet-tile-pane .leaflet-layer { opacity: ${opacity} !important; }
.leaflet-layer[style*="z-index: 500"] canvas { filter: ${isLight ? 'invert(1)' : 'none'}; }
.resources-marker-wrapper {
transform: scale(${scale});
transform-origin: center;
outline: ${showBorders ? `1px solid rgba(${rgb},0.8)` : 'none'};
background-color: rgba(${rgb},0.5);
}
`;
}
applyStyle();
// ========================
// DOM HELPER
// ========================
function el(tag, styles, props) {
const node = document.createElement(tag);
if (styles) Object.assign(node.style, styles);
if (props) Object.assign(node, props);
return node;
}
// ========================
// CONTROLS
// ========================
const ICONS = {
sun: '<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><circle cx="12" cy="12" r="5"/><g stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="3" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="21"/><line x1="3" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="21" y2="12"/><line x1="5.6" y1="5.6" x2="7" y2="7"/><line x1="17" y1="17" x2="18.4" y2="18.4"/><line x1="5.6" y1="18.4" x2="7" y2="17"/><line x1="17" y1="7" x2="18.4" y2="5.6"/></g></svg>',
moon: '<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg>'
};
const PALETTES = {
theme: {
offTrack: '#272727', onTrack: '#ffffff',
offBall: '#ffffff', onBall: '#272727',
icons: { left: [ICONS.sun, '#272727'], right: [ICONS.moon, '#ffffff'] }
},
master: {
offTrack: '#888', onTrack: '#00c853',
offBall: '#ffffff', onBall: '#ffffff'
}
};
function createSlideToggle(initial, onChange, palette) {
const trackOf = v => v ? palette.onTrack : palette.offTrack;
const ballOf = v => v ? palette.onBall : palette.offBall;
const container = el('div', { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', width: '100%' });
const id = 'slide-toggle-' + Math.random().toString(36).slice(2, 11);
const checkbox = el('input', { display: 'none' }, { type: 'checkbox', id, checked: initial });
const label = el('label', {
width: '50px', height: '26px', borderRadius: '50px',
background: trackOf(initial),
position: 'relative', cursor: 'pointer',
display: 'flex', alignItems: 'center',
transition: '0.25s ease'
});
label.htmlFor = id;
if (palette.icons) {
const iconStyle = { flex: '1', display: 'flex', alignItems: 'center', justifyContent: 'center' };
label.append(
el('span', { ...iconStyle, paddingLeft: '4px', color: palette.icons.left[1] }, { innerHTML: palette.icons.left[0] }),
el('span', { ...iconStyle, paddingRight: '4px', color: palette.icons.right[1] }, { innerHTML: palette.icons.right[0] })
);
}
const ball = el('span', {
width: '20px', height: '20px', borderRadius: '50%',
background: ballOf(initial),
position: 'absolute', top: '3px', left: '3px',
transition: 'transform 0.25s ease, background 0.25s ease',
transform: initial ? 'translateX(24px)' : 'translateX(0px)'
});
checkbox.addEventListener('change', () => {
const v = checkbox.checked;
label.style.background = trackOf(v);
ball.style.background = ballOf(v);
ball.style.transform = v ? 'translateX(24px)' : 'translateX(0px)';
onChange(v);
});
label.append(ball);
container.append(checkbox, label);
return container;
}
function createSlider(min, max, step, value, onChange) {
const container = el('div', { display: 'flex', alignItems: 'center', gap: '8px' });
const slider = el('input', null, { type: 'range', min, max, step, value });
const display = el('span', { minWidth: '40px', textAlign: 'right' }, { textContent: parseFloat(value).toFixed(2) });
const updateGradient = (v) => slider.style.setProperty('--value', ((v - min) / (max - min)) * 100 + '%');
updateGradient(value);
slider.addEventListener('input', (e) => {
const v = parseFloat(e.target.value);
display.textContent = v.toFixed(2);
updateGradient(v);
onChange(v);
});
container.append(slider, display);
return container;
}
function createCheckbox(initial, onChange) {
const box = el('div', {
width: '22px', height: '22px', borderRadius: '6px',
cursor: 'pointer', boxSizing: 'border-box', flexShrink: '0',
transition: '0.2s ease'
});
let checked = initial;
const render = () => {
box.style.background = checked ? '#00c853' : '#888';
box.style.border = checked ? '3px solid #fff' : '11px solid #888';
};
render();
box.addEventListener('click', () => {
checked = !checked;
render();
onChange(checked);
});
return box;
}
// ========================
// UI ASSEMBLY
// ========================
function createRow(labelText, control) {
const row = el('div', { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '20px' });
row.append(el('span', { whiteSpace: 'nowrap' }, { textContent: labelText }), control);
return row;
}
function bindOption(key, storageKey, transform = v => v) {
return (v) => {
state[key] = v;
localStorage.setItem(storageKey, transform(v));
applyStyle();
};
}
function createUI() {
if (document.getElementById('map-options-menu')) return true;
const rightItem = document.querySelector('header .text-accent');
if (!rightItem) return false;
const t = getTranslations();
const wrapper = el('div', {
position: 'relative', margin: '0 20px',
color: 'white', cursor: 'pointer',
fontSize: '13px', userSelect: 'none'
}, { id: 'map-options-menu' });
const labelBar = el('div', { display: 'flex', alignItems: 'center', gap: '3px', fontWeight: '600', fontSize: '14px' });
labelBar.append(
el('span', null, { textContent: '⚙️' }),
el('span', null, { textContent: t.options })
);
const dropdown = el('div', {
position: 'absolute', top: 'calc(100% + 24px)', right: '0',
background: '#333', padding: '14px', borderRadius: '10px',
display: 'none', flexDirection: 'column', gap: '12px',
minWidth: '320px', zIndex: '9999'
});
let hideTimeout;
const show = () => { clearTimeout(hideTimeout); dropdown.style.display = 'flex'; };
const hide = () => { hideTimeout = setTimeout(() => dropdown.style.display = 'none', 500); };
[wrapper, dropdown].forEach(n => {
n.addEventListener('mouseenter', show);
n.addEventListener('mouseleave', hide);
});
const subControls = el('div', {
display: 'flex', flexDirection: 'column', gap: '12px',
transition: 'opacity 0.2s ease'
});
const setSubEnabled = (v) => {
subControls.style.opacity = v ? '1' : '0.4';
subControls.style.pointerEvents = v ? 'auto' : 'none';
};
setSubEnabled(state.enabled);
const masterToggle = createSlideToggle(state.enabled, (v) => {
state.enabled = v;
localStorage.setItem('mapEnabled', String(v));
setSubEnabled(v);
applyStyle();
}, PALETTES.master);
subControls.append(
createRow(t.theme, createSlideToggle(state.isLight, bindOption('isLight', 'mapTheme', v => v ? 'light' : 'dark'), PALETTES.theme)),
createRow(t.opacity, createSlider(0.5, 1, 0.05, state.opacity, bindOption('opacity', 'mapOpacity'))),
createRow(t.scale, createSlider(1, 1.5, 0.05, state.scale, bindOption('scale', 'mapScale'))),
createRow(t.border, createCheckbox(state.showBorders, bindOption('showBorders', 'mapBorders')))
);
dropdown.append(
createRow(t.enable, masterToggle),
subControls
);
wrapper.append(labelBar, dropdown);
rightItem.parentNode.insertBefore(wrapper, rightItem);
return true;
}
// ========================
// INIT — polling URL + re-montage automatique après re-render Vue
// ========================
const isMapPage = () => /^\/(fr|en|de|es|pt)\/tools\/map/.test(location.pathname);
let lastPath = null;
function ensureMenu() {
const existing = document.getElementById('map-options-menu');
if (!isMapPage()) existing?.remove();
else if (!existing) createUI();
}
function checkRoute() {
if (location.pathname === lastPath) return;
lastPath = location.pathname;
document.getElementById('map-options-menu')?.remove();
if (isMapPage()) applyStyle(); // remet nos styles en fin de head après une nav SPA
ensureMenu();
}
// Polling URL : capte les changements de route quels qu'ils soient.
setInterval(checkRoute, 250);
// Observer global : si Vue re-rend le header et balaie notre menu, on le remet.
new MutationObserver(ensureMenu).observe(document.body || document.documentElement, { childList: true, subtree: true });
checkRoute();
})();