A context-aware, zero-overhead overlay that instantly redirects social media profiles to alternative frontends (Nitter, Invidious, etc.) and searches across multiple platforms. Includes a built-in settings dashboard and a Smart Tab Title fixer.
// ==UserScript==
// @name OMNI: Social Redirector, Search Bar & Smart Title Fixer
// @namespace https://greasyfork.org/fr/users/1504296-yayornay
// @version 7.2.3
// @description A context-aware, zero-overhead overlay that instantly redirects social media profiles to alternative frontends (Nitter, Invidious, etc.) and searches across multiple platforms. Includes a built-in settings dashboard and a Smart Tab Title fixer.
// @author Mediakeo
// @match *://*.x.com/*
// @match *://*.twitter.com/*
// @match *://*.nitter.net/*
// @match *://*.xcancel.com/*
// @match *://*.nitter.space/*
// @match *://*.youtube.com/*
// @match *://*.yewtu.be/*
// @match *://*.invidious.nerdvpn.de/*
// @match *://*.piped.video/*
// @match *://*.instagram.com/*
// @match *://*.imginn.com/*
// @match *://*.dumpor.io/*
// @match *://*.threads.com/*
// @match *://*.threads.net/*
// @match *://*.bsky.app/*
// @match *://*.tiktok.com/*
// @match *://*.proxitok.pabloferreiro.xyz/*
// @match *://*.twitch.tv/*
// @match *://*.facebook.com/*
// @match *://*.linktr.ee/*
// @match *://*.beacons.ai/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
// 7.2.2 avec options de positionnement et redimensionnement
(function () {
'use strict';
if (window.self !== window.top) return;
// ─────────────────────────────────────────────
// 1. DEFAULTS & CONFIG — lus une seule fois
// ─────────────────────────────────────────────
const D = {
master: 'true',
fontSize: '11px',
// ── Position & Taille ────────────────────────
scale_val: '1.0', // Facteur d'échelle (ex: 0.8 pour 80%)
anchor_y: 'top', // 'top' ou 'bottom'
pos_y: '55px', // 'px' ou '%'
anchor_x: 'right', // 'left' ou 'right'
pos_x: '20px', // 'px' ou '%'
// ─────────────────────────────────────────────
fixed: 'true',
opacity: '0.92',
show_rx: 'true',
show_x: 'true',
show_yt: 'true',
show_ig: 'true',
show_tt: 'true',
show_tw: 'true',
show_lt: 'true',
// ── Smart Title Fix ──────────────────────────
// master : active/désactive toute la fonctionnalité
// title_* : active/désactive site par site (via menu TM)
// Reddit intentionnellement absent : aucune détection sur ce site
title_fix: 'true',
title_fb: 'true',
title_ig: 'true',
title_x: 'true',
title_th: 'true',
title_tt: 'true',
title_tw: 'true',
title_bsky: 'true',
title_lt: 'true',
// ── Instances ────────────────────────────────
rx_inst: 'Facebook:facebook.com,Mastodon:mastodon.social,Threads:threads.net,Bluesky:bsky.app,Google:google.com',
x_inst: 'X:x.com,Twitter:twitter.com,nitter:nitter.net,xcancel:xcancel.com,nitter.space:nitter.space',
yt_inst: 'YouTube:youtube.com,yewtu.be:yewtu.be,invidious:invidious.nerdvpn.de,piped:piped.video',
ig_inst: 'Instagram:instagram.com,imginn:imginn.com,dumpor:dumpor.io',
tt_inst: 'TikTok:tiktok.com,proxitok:proxitok.pabloferreiro.xyz,tokfurious:tokfurious.ch',
tw_inst: 'Twitch:twitch.tv,TwTracker:twitchtracker.com,SullyGnome:sullygnome.com/channel,StrCharts:streamscharts.com/channels',
lt_inst: 'Linktree:linktr.ee,Beacons:beacons.ai',
socials: 'BSky:https://bsky.app/search?q={{USER}},Insta:https://www.instagram.com/{{USER}}/,THD:https://www.threads.net/@{{USER}},TTK:https://www.tiktok.com/@{{USER}},MAS:https://mastodon.social/search?q=%40{{USER}},GOO:https://www.google.com/search?q=%22{{USER}}%22,YT:https://www.youtube.com/results?search_query=%22{{USER}}%22,RDT:https://www.reddit.com/search/?q={{USER}},TW:https://www.twitch.tv/{{USER}},X:https://x.com/search?q={{USER}},FB:https://www.facebook.com/search/top?q={{USER}},LKT:https://linktr.ee/{{USER}}',
custom_1: 'Reddit|#ff4500|https://{{DOM}}/user/{{USER}} >>> Libreddit:libreddit.spike.codes',
custom_2: '',
};
const CFG = {};
for (const k in D) CFG[k] = GM_getValue(k, D[k]);
const masterEnabled = CFG.master === 'true';
const isFixed = CFG.fixed === 'true';
// ─────────────────────────────────────────────
// 2. CSS HOVER — injecté une seule fois,
// remplace les N×2 event listeners JS
// ─────────────────────────────────────────────
const style = document.createElement('style');
style.textContent = '#omni-bar a:hover{background-color:rgba(255,255,255,0.12)!important}';
document.head.appendChild(style);
// ─────────────────────────────────────────────
// 3. PARSERS — exécutés une seule fois
// indexOf au lieu de split(':') pour les
// domaines contenant un slash (sullygnome…)
// ─────────────────────────────────────────────
function parseInstances(raw) {
if (!raw) return [];
return raw.split(',').map(item => {
const i = item.indexOf(':');
if (i === -1) return null;
const name = item.slice(0, i).trim();
const domain = item.slice(i + 1).trim();
return (name && domain) ? { name, domain } : null;
}).filter(Boolean);
}
function parseCustomRow(raw) {
if (!raw || !raw.includes('>>>')) return null;
const sep = raw.indexOf('>>>');
const cfg = raw.slice(0, sep);
const inst = raw.slice(sep + 3);
const [label, color, template] = cfg.split('|').map(s => s.trim());
const instances = parseInstances(inst);
if (!label || !template || !instances.length) return null;
return { label, color: color || '#ffffff', template, instances };
}
function parseSocials(raw) {
return raw.split(',').map(item => {
const i = item.indexOf(':');
if (i === -1) return null;
return { name: item.slice(0, i).trim(), urlPattern: item.slice(i + 1).trim() };
}).filter(Boolean);
}
// Résultats mis en cache — jamais re-parsés
const INST = {
rx: parseInstances(CFG.rx_inst),
x: parseInstances(CFG.x_inst),
yt: parseInstances(CFG.yt_inst),
ig: parseInstances(CFG.ig_inst),
tt: parseInstances(CFG.tt_inst),
tw: parseInstances(CFG.tw_inst),
lt: parseInstances(CFG.lt_inst),
};
const CUSTOM1 = parseCustomRow(CFG.custom_1);
const CUSTOM2 = parseCustomRow(CFG.custom_2);
const SOCIALS = parseSocials(CFG.socials);
// ─────────────────────────────────────────────
// 4. COULEURS — Map O(1), précalculée
// ─────────────────────────────────────────────
const COLOR_MAP = new Map([
['BSKY','#1d54a5'], ['BLUESKY','#1d54a5'],
['INSTA','#e1306c'], ['DUMPOR','#e1306c'], ['IMGINN','#e1306c'],
['THD','#ffcc00'], ['THREADS','#ffcc00'],
['TTK','#00f2fe'], ['TIKTOK','#00f2fe'],
['MAS','#2ebd59'], ['MASTODON','#2ebd59'],
['GOO','#4285f4'], ['GOOGLE','#4285f4'],
['YT','#ff3333'], ['YOUTUBE','#ff3333'],
['RDT','#ff4500'], ['REDDIT','#ff4500'],
['TW','#9146ff'], ['TWITCH','#9146ff'],
['X','#ffffff'], ['TWITTER','#ffffff'],
['FB','#1877f2'], ['FACEBOOK','#1877f2'],
['LKT','#39e09b'], ['LINKTREE','#39e09b'],
['BEACONS','#ff007f'],
]);
function getSocialColor(name) {
const n = name.toUpperCase();
if (COLOR_MAP.has(n)) return COLOR_MAP.get(n);
for (const [k, v] of COLOR_MAP) { if (n.includes(k)) return v; }
return '#ffffff';
}
// ─────────────────────────────────────────────
// 5. BLACKLIST — Set O(1)
// ─────────────────────────────────────────────
const BLACKLIST = new Set([
'home','explore','notifications','messages','i','settings','search',
'tos','privacy','about','lists','feed','trending','channel','watch',
'shorts','reels','p','direct','accounts','embed','directory','videos',
'clips','downloads','jobs','profile','photo.php','permalink.php',
'groups','pages','marketplace','events','share','people',
]);
// ─────────────────────────────────────────────
// 6. SMART TITLE FIX
// Écrit document.title avec le vrai nom de page
// extrait du DOM, au lieu du générique "Facebook".
//
// Reddit : absent intentionnellement.
// → site='unknown' dans getContext() → barre cachée
// → aucune tentative de correction de titre
//
// Note : les sélecteurs CSS peuvent casser lors
// d'une mise à jour du site. Si c'est le cas,
// inspecter le h1/h2 avec DevTools et corriger
// le sélecteur dans TITLE_SELECTORS ci-dessous.
// ─────────────────────────────────────────────
const TITLE_SELECTORS = {
'facebook.com': { sel: 'h1', key: 'title_fb' },
'instagram.com': { sel: 'header h2, h1, h2', key: 'title_ig' },
'x.com': { sel: '[data-testid="UserName"] span:first-child, h2', key: 'title_x' },
'twitter.com': { sel: '[data-testid="UserName"] span:first-child, h2', key: 'title_x' },
'threads.net': { sel: 'h1, header h2', key: 'title_th' },
'threads.com': { sel: 'h1, header h2', key: 'title_th' },
'tiktok.com': { sel: 'h1[data-e2e="user-title"], h1', key: 'title_tt' },
'twitch.tv': { sel: 'h1', key: 'title_tw' },
'bsky.app': { sel: 'h1', key: 'title_bsky' },
'linktr.ee': { sel: 'h1', key: 'title_lt' },
'beacons.ai': { sel: 'h1', key: 'title_lt' },
};
// Noms génériques à ne pas écrire dans le titre
const TITLE_GENERIC = new Set([
'Facebook','Instagram','X','Twitter','Threads','TikTok','Twitch',
'Bluesky','Linktree','Beacons','Home','Explore','Search','Feed',
]);
function fixTitle() {
if (CFG.title_fix !== 'true') return;
const host = window.location.hostname;
const entry = Object.entries(TITLE_SELECTORS).find(([domain]) => host.includes(domain));
if (!entry) return;
const [domain, { sel, key }] = entry;
if (CFG[key] !== 'true') return;
let name = null;
// 1. Scanner TOUS les éléments (indispensable pour FB qui cache le 1er h1)
const elements = document.querySelectorAll(sel);
for (const el of elements) {
// innerText ignore les balises invisibles, textContent est le fallback
const txt = el.innerText?.trim() || el.textContent?.trim();
// Si on a du texte valide et qu'il n'est pas dans la blacklist générique
if (txt && txt.length >= 2 && !TITLE_GENERIC.has(txt)) {
name = txt;
break; // On a trouvé le vrai titre, on sort de la boucle
}
}
// 2. Fallback sur les Meta Tags (si le DOM est lent mais le header est chargé)
if (!name) {
const meta = document.querySelector('meta[property="og:title"]');
if (meta && meta.content && !TITLE_GENERIC.has(meta.content)) {
name = meta.content;
}
}
// 3. Nettoyage spécifique selon la plateforme
if (name) {
if (domain === 'facebook.com') {
// Enlève " | Facebook", " - Facebook", et le préfixe "Groupe "
name = name.split(' | ')[0].split(' - ')[0].replace(/^Groupe\s+/i, '').trim();
} else {
// Nettoyage générique pour les autres (ex: supprime "@username" à la fin)
name = name.replace(/(@|\/).*/, '').trim();
}
}
// 4. Injection dans l'onglet
if (name && name.length >= 2 && !TITLE_GENERIC.has(name)) {
if (document.title !== name) {
document.title = name;
}
}
}
// ─────────────────────────────────────────────
// 7. EXTRACTION DU CONTEXTE
// Appelée uniquement sur changement d'URL.
// Lit document.title APRÈS fixTitle() pour
// bénéficier du titre corrigé dans searchName.
// ─────────────────────────────────────────────
function getContext() {
const host = window.location.hostname;
const path = window.location.pathname;
const href = window.location.href;
const segments = path.split('/').filter(Boolean);
const title = document.title || '';
let site = 'unknown', handle = null, searchName = null;
let fbProfileId = null, ytChannelPath = null;
if (host.includes('facebook.com') || INST.rx.some(i => host.includes(i.domain))) {
site = 'facebook';
const m = href.match(/id=(\d+)/);
if (path.includes('profile.php') && m) fbProfileId = 'profile.php?id=' + m[1];
else handle = segments[0];
} else if (host.includes('youtube.com')) {
if (!path.startsWith('/watch') && !path.startsWith('/shorts') && !path.startsWith('/live')) {
site = 'youtube';
if (['a','c','user'].includes(segments[0])) handle = segments[1];
else if (segments[0]?.startsWith('@')) handle = segments[0];
else if (segments[0] === 'channel' && segments[1]) ytChannelPath = 'channel/' + segments[1];
else handle = segments[0];
}
} else if (host.includes('x.com') || host.includes('twitter.com') || INST.x.some(i => host.includes(i.domain))) {
site = 'x'; handle = segments[0];
} else if (host.includes('instagram.com') || INST.ig.some(i => host.includes(i.domain))) {
site = 'instagram';
handle = host.includes('dumpor.io')
? (segments[0] === 'user' ? segments[1] : segments[0])
: (['user','org'].includes(segments[0]) ? segments[1] : segments[0]);
} else if (host.includes('threads.com') || host.includes('threads.net')) {
site = 'threads'; handle = segments[0];
} else if (host.includes('bsky.app')) {
site = 'bluesky'; if (segments[0] === 'profile') handle = segments[1];
} else if (host.includes('tiktok.com') || INST.tt.some(i => host.includes(i.domain))) {
site = 'tiktok'; handle = segments[0];
} else if (host.includes('twitch.tv')) {
site = 'twitch'; handle = segments[0];
} else if (host.includes('linktr.ee') || host.includes('beacons.ai') || INST.lt.some(i => host.includes(i.domain))) {
site = 'linktree'; handle = segments[0];
} else if (segments.some(s => s.startsWith('@'))) {
site = 'mastodon';
handle = segments.find(s => s.startsWith('@')).slice(1);
}
// Nettoyage handle
if (handle) {
handle = handle.replace(/^@/, '').split('?')[0];
if (BLACKLIST.has(handle.toLowerCase()) || handle.length < 2) handle = null;
}
// searchName depuis le titre (potentiellement déjà corrigé par fixTitle)
const GENERIC = new Set(['X','Twitter','Instagram','TikTok','Facebook','Twitch','YouTube','Threads','Bluesky']);
const raw = title.split(' | ')[0].split(' - ')[0].split(' (')[0].replace(/(@|\/).*/, '').trim();
searchName = (!raw || GENERIC.has(raw)) ? null : raw;
if (!handle && searchName) handle = searchName;
if (!searchName && handle) searchName = handle;
return { site, handle, searchName, isFallback: !handle || handle === searchName, fbProfileId, ytChannelPath };
}
// ─────────────────────────────────────────────
// 8. CRÉATION DE LIENS
// rel="noopener noreferrer" sur tous les liens
// ─────────────────────────────────────────────
function createLink(href, text, color) {
const a = document.createElement('a');
a.href = href; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.textContent = text;
a.style.cssText = `color:${color};text-decoration:none;padding:1px 3px;border-radius:3px;transition:background-color 0.15s`;
return a;
}
function createRow(instances, ctx, baseTemplate, labelText, labelColor, useIndividualColors) {
const { handle, isFallback, fbProfileId, ytChannelPath } = ctx;
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:2px';
const lbl = document.createElement('span');
lbl.textContent = labelText;
lbl.style.cssText = `color:${labelColor};margin-right:1px`;
row.appendChild(lbl);
instances.forEach((inst, idx) => {
const dom = inst.domain.toLowerCase();
const enc = handle ? encodeURIComponent(handle) : '';
let url = `https://${inst.domain}`;
if (handle || (dom === 'facebook.com' && fbProfileId) || ytChannelPath) {
if (isFallback || !handle) {
if (dom === 'facebook.com') url = fbProfileId ? `https://www.facebook.com/${fbProfileId}` : `https://www.facebook.com/search/top?q=${enc}`;
else if (dom.includes('youtube') || dom.includes('yewtu') || dom.includes('invidious') || dom.includes('piped')) url = ytChannelPath ? `https://${inst.domain}/${ytChannelPath}` : `https://${inst.domain}/results?search_query=${enc}`;
else if (dom.includes('x.com') || dom.includes('twitter') || dom.includes('nitter') || dom.includes('xcancel')) url = `https://${inst.domain}/search?q=${enc}`;
else if (dom === 'instagram.com') url = `https://www.instagram.com/search/?q=${enc}`;
else if (dom.includes('imginn')) url = `https://imginn.com/search/?q=${enc}`;
else if (dom.includes('dumpor')) url = `https://dumpor.io/search?query=${enc}`;
else if (dom.includes('threads')) url = `https://www.threads.net/search?q=${enc}`;
else if (dom === 'bsky.app') url = `https://bsky.app/search?q=${enc}`;
else if (dom.includes('tiktok') || dom.includes('proxitok') || dom.includes('tokfurious')) url = `https://${inst.domain}/search?q=${enc}`;
else if (dom.includes('twitch')) url = `https://${inst.domain}/search?term=${enc}`;
else if (dom === 'mastodon.social') url = `https://mastodon.social/search?q=${enc}`;
else if (dom === 'google.com') url = `https://www.google.com/search?q=%22${enc}%22`;
else if (dom === 'linktr.ee' || dom === 'beacons.ai') url = `https://www.google.com/search?q=site:${inst.domain}+${enc}`;
else url = baseTemplate.replace('{{DOM}}', inst.domain).replace('{{USER}}', enc);
} else {
if (dom.includes('imginn')) url = `https://imginn.com/${handle}/`;
else if (dom.includes('dumpor')) url = `https://dumpor.io/user/${handle}`;
else if (dom === 'instagram.com') url = `https://instagram.com/${handle}/`;
else if (dom === 'tiktok.com') url = `https://www.tiktok.com/@${handle}`;
else if (dom === 'youtube.com') url = `https://www.youtube.com/${handle.startsWith('@') ? '' : '@'}${handle}`;
else if (dom === 'facebook.com') url = fbProfileId ? `https://www.facebook.com/${fbProfileId}` : `https://www.facebook.com/${handle}`;
else if (dom === 'mastodon.social') url = `https://mastodon.social/search?q=${enc}`;
else if (dom === 'threads.net') url = `https://www.threads.net/@${handle}`;
else if (dom === 'bsky.app') url = `https://bsky.app/profile/${handle}`;
else if (dom === 'google.com') url = `https://www.google.com/search?q="${enc}"`;
else url = baseTemplate.replace('{{DOM}}', inst.domain).replace('{{USER}}', handle);
}
}
row.appendChild(createLink(url, inst.name, useIndividualColors ? getSocialColor(inst.name) : labelColor));
if (idx < instances.length - 1) {
const sep = document.createElement('span');
sep.textContent = '·'; sep.style.color = 'rgba(255,255,255,0.15)';
row.appendChild(sep);
}
});
return row;
}
// ─────────────────────────────────────────────
// 9. BARRE UI
// Créée une fois. Rebuild uniquement si l'URL
// a réellement changé depuis le dernier rendu.
// ─────────────────────────────────────────────
let bar = null;
let lastBuilt = null;
function createBar() {
bar = document.createElement('div');
bar.id = 'omni-bar';
bar.style.cssText = [
`position:${isFixed ? 'fixed' : 'absolute'}`,
// Placement Dynamique
`${CFG.anchor_y}:${CFG.pos_y}`,
`${CFG.anchor_x}:${CFG.pos_x}`,
// Echelle et Point d'ancrage du redimensionnement
`transform:scale(${CFG.scale_val})`,
`transform-origin:${CFG.anchor_y} ${CFG.anchor_x}`,
// Reste du design
'z-index:999999',
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif',
`font-size:${CFG.fontSize}`,
'font-weight:bold',
'display:none',
'flex-direction:column',
'gap:3px',
`background-color:rgba(5,5,5,${CFG.opacity})`,
'padding:5px 8px',
'border-radius:8px',
'box-shadow:0 4px 15px rgba(0,0,0,0.6)',
'backdrop-filter:blur(6px)',
'color:#ffffff',
'border:1px solid rgba(255,255,255,0.08)',
].join(';');
document.body.appendChild(bar);
}
function updateBar(ctx) {
if (!bar) return;
if (!masterEnabled || document.fullscreenElement || ctx.site === 'unknown') {
bar.style.display = 'none';
return;
}
const currentUrl = location.href;
if (currentUrl === lastBuilt) return;
lastBuilt = currentUrl;
bar.innerHTML = '';
if (ctx.searchName && SOCIALS.length) {
const searchRow = document.createElement('div');
searchRow.style.cssText = 'display:flex;align-items:center;flex-wrap:wrap;max-width:420px;gap:3px;border-bottom:1px solid rgba(255,255,255,0.1);padding-bottom:4px;margin-bottom:1px';
SOCIALS.forEach(link => {
const url = link.urlPattern.replace('{{USER}}', encodeURIComponent(ctx.searchName));
searchRow.appendChild(createLink(url, link.name, getSocialColor(link.name)));
});
bar.appendChild(searchRow);
}
if (CFG.show_rx === 'true' && INST.rx.length) bar.appendChild(createRow(INST.rx, ctx, 'https://{{DOM}}/{{USER}}', 'RX:', '#ffffff', true));
if (CFG.show_x === 'true' && INST.x.length) bar.appendChild(createRow(INST.x, ctx, 'https://{{DOM}}/{{USER}}', 'X:', '#ff8800', false));
if (CFG.show_yt === 'true' && INST.yt.length) bar.appendChild(createRow(INST.yt, ctx, 'https://{{DOM}}/{{USER}}', 'YT:', '#ff3333', false));
if (CFG.show_ig === 'true' && INST.ig.length) bar.appendChild(createRow(INST.ig, ctx, 'https://{{DOM}}/user/{{USER}}', 'IG:', '#e1306c', false));
if (CFG.show_tt === 'true' && INST.tt.length) bar.appendChild(createRow(INST.tt, ctx, 'https://{{DOM}}/@{{USER}}', 'TT:', '#00f2fe', false));
if (CFG.show_tw === 'true' && INST.tw.length) bar.appendChild(createRow(INST.tw, ctx, 'https://{{DOM}}/{{USER}}', 'TW:', '#9146ff', false));
if (CFG.show_lt === 'true' && INST.lt.length) bar.appendChild(createRow(INST.lt, ctx, 'https://{{DOM}}/{{USER}}', 'BIO:', '#39e09b', false));
if (CUSTOM1) bar.appendChild(createRow(CUSTOM1.instances, ctx, CUSTOM1.template, `${CUSTOM1.label}:`, CUSTOM1.color, false));
if (CUSTOM2) bar.appendChild(createRow(CUSTOM2.instances, ctx, CUSTOM2.template, `${CUSTOM2.label}:`, CUSTOM2.color, false));
bar.style.display = bar.children.length ? 'flex' : 'none';
}
// ─────────────────────────────────────────────
// 10. NAVIGATION SPA — zéro polling
// fixTitle() appelé AVANT getContext() :
// le titre corrigé est disponible pour searchName.
// Fallback à +600ms si searchName était vide.
// ─────────────────────────────────────────────
let navTimer = null;
let fallbackTimer = null;
let lastUrl = location.href;
function onNavigate() {
const newUrl = location.href;
if (newUrl === lastUrl) return;
lastUrl = newUrl;
lastBuilt = null;
clearTimeout(navTimer);
clearTimeout(fallbackTimer);
navTimer = setTimeout(() => {
fixTitle(); // ← corrige le titre avant de lire le contexte
const ctx = getContext();
updateBar(ctx);
if (!ctx.searchName) {
fallbackTimer = setTimeout(() => {
lastBuilt = null;
fixTitle(); // ← second essai pour les SPAs lentes
updateBar(getContext());
}, 600);
}
}, 600);
}
const _push = history.pushState.bind(history);
const _replace = history.replaceState.bind(history);
history.pushState = (...a) => { _push(...a); onNavigate(); };
history.replaceState = (...a) => { _replace(...a); onNavigate(); };
window.addEventListener('popstate', onNavigate);
document.addEventListener('fullscreenchange', () => {
if (!bar) return;
bar.style.display = document.fullscreenElement ? 'none' : (bar.children.length ? 'flex' : 'none');
});
// ─────────────────────────────────────────────
// 11. DASHBOARD MODAL
// ─────────────────────────────────────────────
function openDashboard() {
if (document.getElementById('omni-dashboard')) return;
const backdrop = document.createElement('div');
backdrop.id = 'omni-dashboard';
backdrop.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.75);z-index:9999999;display:flex;align-items:center;justify-content:center;font-family:sans-serif';
const win = document.createElement('div');
win.style.cssText = 'background:#16161a;border:1px solid #2a2a35;border-radius:12px;width:650px;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,0.5);color:#fff;overflow:hidden';
const header = document.createElement('div');
header.textContent = '⚙️ OMNI v7.2.2 — Platform Manager';
header.style.cssText = 'padding:15px;border-bottom:1px solid #2a2a35;font-weight:bold;font-size:14px;background:#1e1e24';
win.appendChild(header);
const formArea = document.createElement('div');
formArea.style.cssText = 'padding:15px;overflow-y:auto;flex:1;display:flex;flex-direction:column;gap:12px';
const fields = [
{ key: 'scale_val', label: '— Taille globale (ex: 1.0, 0.8 pour 80%)', type: 'text' },
{ key: 'anchor_y', label: '— Ancrage Vertical (top ou bottom)', type: 'text' },
{ key: 'pos_y', label: '— Position Verticale (ex: 55px, 5%)', type: 'text' },
{ key: 'anchor_x', label: '— Ancrage Horizontal (left ou right)', type: 'text' },
{ key: 'pos_x', label: '— Position Horizontale (ex: 20px, 2%)', type: 'text' },
{ key: 'socials', label: '1. Search bar (NAME:URL,...)', type: 'textarea' },
{ key: 'rx_inst', label: '2. RX row (Facebook, Mastodon…)', type: 'text' },
{ key: 'x_inst', label: '3. X / Twitter row', type: 'text' },
{ key: 'yt_inst', label: '4. YouTube row', type: 'text' },
{ key: 'ig_inst', label: '5. Instagram / mirrors row', type: 'text' },
{ key: 'tt_inst', label: '6. TikTok row', type: 'text' },
{ key: 'tw_inst', label: '7. Twitch row', type: 'text' },
{ key: 'lt_inst', label: '8. Bio links row', type: 'text' },
{ key: 'custom_1', label: 'Custom row 1', type: 'text' },
{ key: 'custom_2', label: 'Custom row 2', type: 'text' },
{ key: 'title_fix', label: '— Smart Title Fix master (true/false) · toggles par site : menu TM', type: 'text' },
];
const ctrlStyle = 'width:100%;background:#22222b;border:1px solid #3a3a4a;border-radius:6px;color:#fff;padding:6px 8px;font-size:11px;box-sizing:border-box';
fields.forEach(f => {
const block = document.createElement('div');
const lbl = document.createElement('label');
lbl.textContent = f.label;
lbl.style.cssText = 'display:block;font-size:11px;color:#aaa;margin-bottom:4px;font-weight:bold';
block.appendChild(lbl);
const ctrl = document.createElement(f.type === 'textarea' ? 'textarea' : 'input');
if (f.type !== 'textarea') ctrl.type = 'text';
if (f.type === 'textarea') ctrl.style.height = '60px';
ctrl.id = 'omni_' + f.key;
ctrl.value = CFG[f.key] || '';
ctrl.style.cssText = ctrlStyle;
block.appendChild(ctrl);
formArea.appendChild(block);
});
win.appendChild(formArea);
const footer = document.createElement('div');
footer.style.cssText = 'padding:12px;border-top:1px solid #2a2a35;display:flex;justify-content:flex-end;gap:8px;background:#1e1e24';
const cancel = document.createElement('button');
cancel.textContent = 'Cancel';
cancel.style.cssText = 'padding:6px 12px;background:#3a3a4a;border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:11px';
cancel.addEventListener('click', () => backdrop.remove());
const save = document.createElement('button');
save.textContent = '💾 Save & Reload';
save.style.cssText = 'padding:6px 12px;background:#1877f2;border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:11px;font-weight:bold';
save.addEventListener('click', () => {
fields.forEach(f => GM_setValue(f.key, document.getElementById('omni_' + f.key).value));
backdrop.remove();
location.reload();
});
footer.appendChild(cancel);
footer.appendChild(save);
win.appendChild(footer);
// LIGNE MANQUANTE : On attache la fenêtre au fond gris
backdrop.appendChild(win);
document.body.appendChild(backdrop);
}
// ─────────────────────────────────────────────
// 12. MENUS TAMPERMONKEY
// ─────────────────────────────────────────────
GM_registerMenuCommand('⚙️ Open OMNI Manager', openDashboard);
GM_registerMenuCommand(masterEnabled ? '🔴 Disable OMNI' : '🟢 Enable OMNI', () => { GM_setValue('master', masterEnabled ? 'false' : 'true'); location.reload(); });
GM_registerMenuCommand(isFixed ? '🔓 Detach bar (absolute)' : '🔒 Fix bar (fixed)', () => { GM_setValue('fixed', isFixed ? 'false' : 'true'); location.reload(); });
// Barre de navigation — toggles par ligne
const toggles = [
['show_rx','RX'],['show_x','X/Twitter'],['show_yt','YouTube'],
['show_ig','Instagram'],['show_tt','TikTok'],['show_tw','Twitch'],['show_lt','Bio Links'],
];
toggles.forEach(([k, label]) => {
const on = CFG[k] === 'true';
GM_registerMenuCommand(`${on ? '❌ Hide' : '✅ Show'} ${label} row`, () => { GM_setValue(k, on ? 'false' : 'true'); location.reload(); });
});
// Smart Title Fix — master toggle
const titleFixOn = CFG.title_fix === 'true';
GM_registerMenuCommand(
`${titleFixOn ? '🔴 Disable' : '🟢 Enable'} Smart Title Fix`,
() => { GM_setValue('title_fix', titleFixOn ? 'false' : 'true'); location.reload(); }
);
// Smart Title Fix — toggles par site (visibles seulement si master actif)
if (titleFixOn) {
const titleToggles = [
['title_fb','Facebook'],['title_ig','Instagram'],['title_x','X/Twitter'],
['title_th','Threads'],['title_tt','TikTok'],['title_tw','Twitch'],
['title_bsky','Bluesky'],['title_lt','Bio Links'],
];
titleToggles.forEach(([k, label]) => {
const on = CFG[k] === 'true';
GM_registerMenuCommand(
` ${on ? '❌' : '✅'} Title: ${label}`,
() => { GM_setValue(k, on ? 'false' : 'true'); location.reload(); }
);
});
}
// ─────────────────────────────────────────────
// 13. INIT
// ─────────────────────────────────────────────
function init() {
createBar();
const titleBefore = document.title;
updateBar(getContext());
// Title fix avec délai : le DOM SPA peut ne pas être prêt au chargement initial
setTimeout(() => {
fixTitle();
// Rebuild uniquement si le titre a réellement changé
if (document.title !== titleBefore) {
lastBuilt = null;
updateBar(getContext());
}
}, 700);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();