Skip to content

Instantly share code, notes, and snippets.

@konfiot
Last active July 19, 2026 20:28
Show Gist options
  • Select an option

  • Save konfiot/0e756989ceb9257afdbe542e64e45694 to your computer and use it in GitHub Desktop.

Select an option

Save konfiot/0e756989ceb9257afdbe542e64e45694 to your computer and use it in GitHub Desktop.
Vinted filtre
// ==UserScript==
// @name Vinted - Filtre de marques exclues
// @namespace vinted-brand-filter
// @version 1.2
// @description Masque automatiquement les annonces Vinted dont la marque fait partie d'une liste d'exclusion configurable
// @author toi
// @match https://www.vinted.fr/*
// @match https://www.vinted.com/*
// @match https://www.vinted.be/*
// @match https://www.vinted.de/*
// @match https://www.vinted.it/*
// @match https://www.vinted.es/*
// @match https://www.vinted.co.uk/*
// @match https://www.vinted.pl/*
// @match https://www.vinted.nl/*
// @match https://www.vinted.at/*
// @match https://www.vinted.cz/*
// @match https://www.vinted.pt/*
// @match https://www.vinted.lt/*
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'vinted_excluded_brands';
// --- Stockage (GM_* si dispo, sinon localStorage en secours) ---
const hasGM = typeof GM_getValue === 'function' && typeof GM_setValue === 'function';
function loadBrands() {
try {
const raw = hasGM ? GM_getValue(STORAGE_KEY, '[]') : (localStorage.getItem(STORAGE_KEY) || '[]');
const arr = JSON.parse(raw);
return Array.isArray(arr) ? arr : [];
} catch (e) {
return [];
}
}
function saveBrands(brands) {
const raw = JSON.stringify(brands);
// On écrit dans les deux stockages disponibles pour une robustesse maximale :
// GM_setValue persiste indépendamment du site (recommandé), localStorage sert de secours.
if (hasGM) GM_setValue(STORAGE_KEY, raw);
try { localStorage.setItem(STORAGE_KEY, raw); } catch (e) { /* ignore */ }
flashSaved();
}
function flashSaved() {
if (!savedIndicator) return;
savedIndicator.textContent = '✓ enregistré';
savedIndicator.style.opacity = '1';
clearTimeout(flashSaved._t);
flashSaved._t = setTimeout(() => {
savedIndicator.style.opacity = '0';
}, 1200);
}
let savedIndicator = null;
let excludedBrands = loadBrands();
// --- Styles ---
const style = document.createElement('style');
style.textContent = `
#vbf-toggle {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 999999;
background: #09B1BA;
color: #fff;
border: none;
border-radius: 999px;
padding: 12px 18px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 14px rgba(0,0,0,0.25);
font-family: Arial, sans-serif;
}
#vbf-toggle:hover { background: #078e95; }
#vbf-panel {
position: fixed;
bottom: 76px;
right: 24px;
z-index: 999999;
width: 300px;
max-height: 420px;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
font-family: Arial, sans-serif;
display: none;
flex-direction: column;
overflow: hidden;
}
#vbf-panel.open { display: flex; }
#vbf-header {
padding: 12px 14px;
background: #09B1BA;
color: #fff;
font-weight: 700;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
}
#vbf-header span.vbf-count {
font-weight: 400;
font-size: 12px;
opacity: 0.9;
}
#vbf-body {
padding: 10px 14px;
overflow-y: auto;
flex: 1;
}
#vbf-input-row {
display: flex;
gap: 6px;
margin-bottom: 10px;
}
#vbf-input {
flex: 1;
padding: 8px 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 13px;
}
#vbf-add {
background: #09B1BA;
color: #fff;
border: none;
border-radius: 6px;
padding: 0 12px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
}
#vbf-add:hover { background: #078e95; }
.vbf-tag {
display: flex;
justify-content: space-between;
align-items: center;
background: #f5f5f5;
border-radius: 6px;
padding: 6px 10px;
margin-bottom: 6px;
font-size: 13px;
color: #222;
}
.vbf-tag button {
background: none;
border: none;
color: #d0021b;
font-size: 15px;
cursor: pointer;
line-height: 1;
padding: 0 0 0 8px;
}
#vbf-empty {
color: #888;
font-size: 12.5px;
text-align: center;
padding: 10px 0;
}
#vbf-hidden-count {
padding: 8px 14px;
font-size: 12px;
color: #555;
border-top: 1px solid #eee;
background: #fafafa;
}
#vbf-saved {
font-size: 11px;
font-weight: 400;
color: #d4f7f9;
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
`;
document.head.appendChild(style);
// --- UI ---
const toggleBtn = document.createElement('button');
toggleBtn.id = 'vbf-toggle';
toggleBtn.textContent = '🚫 Marques';
document.body.appendChild(toggleBtn);
const panel = document.createElement('div');
panel.id = 'vbf-panel';
panel.innerHTML = `
<div id="vbf-header">
<span>Marques exclues <span class="vbf-count" id="vbf-count-label"></span><span id="vbf-saved"></span></span>
<span style="cursor:pointer;" id="vbf-close">✕</span>
</div>
<div id="vbf-body">
<div id="vbf-input-row">
<input id="vbf-input" type="text" placeholder="Ex: Nike, Shein..." />
<button id="vbf-add">Ajouter</button>
</div>
<div id="vbf-list"></div>
</div>
<div id="vbf-hidden-count"></div>
`;
document.body.appendChild(panel);
const input = panel.querySelector('#vbf-input');
const addBtn = panel.querySelector('#vbf-add');
const listEl = panel.querySelector('#vbf-list');
const countLabel = panel.querySelector('#vbf-count-label');
const hiddenCountEl = panel.querySelector('#vbf-hidden-count');
savedIndicator = panel.querySelector('#vbf-saved');
toggleBtn.addEventListener('click', () => panel.classList.toggle('open'));
panel.querySelector('#vbf-close').addEventListener('click', () => panel.classList.remove('open'));
function renderList() {
countLabel.textContent = `(${excludedBrands.length})`;
if (excludedBrands.length === 0) {
listEl.innerHTML = '<div id="vbf-empty">Aucune marque exclue pour le moment.</div>';
return;
}
listEl.innerHTML = '';
excludedBrands.forEach((brand, i) => {
const tag = document.createElement('div');
tag.className = 'vbf-tag';
tag.innerHTML = `<span>${escapeHtml(brand)}</span><button data-i="${i}">✕</button>`;
listEl.appendChild(tag);
});
listEl.querySelectorAll('button[data-i]').forEach((btn) => {
btn.addEventListener('click', () => {
const i = parseInt(btn.getAttribute('data-i'), 10);
excludedBrands.splice(i, 1);
saveBrands(excludedBrands);
renderList();
filterItems();
});
});
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function addBrand() {
const val = input.value.trim();
if (!val) return;
const exists = excludedBrands.some((b) => b.toLowerCase() === val.toLowerCase());
if (!exists) {
excludedBrands.push(val);
saveBrands(excludedBrands);
renderList();
filterItems();
}
input.value = '';
input.focus();
}
addBtn.addEventListener('click', addBrand);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') addBrand();
});
renderList();
// --- Détection des annonces individuelles ---
// Important : sur Vinted, certains blocs regroupent plusieurs objets d'un
// même vendeur ("encart vendeur"). Si on cible directement ces blocs, cacher
// UN objet caché tout le paquet (y compris les objets valides). On cible donc
// en priorité le lien précis de chaque objet (le plus "profond" dans le DOM),
// puis on remonte jusqu'au plus petit conteneur qui n'englobe QUE cet objet.
const LEAF_SELECTORS = [
'a[data-testid$="--overlay-link"]',
'a[data-testid*="overlay-link"]',
'[data-testid^="product-item"] a[title]',
'[data-testid^="product-item"]',
'[data-testid="grid-item"] a[title]',
'[data-testid="grid-item"]',
'.new-item-box__container a[title]',
'.new-item-box__container',
];
// On mémorise le sélecteur qui a fonctionné pour ne pas sauter d'un sélecteur
// à l'autre à chaque passage (source de "clignotement" du compteur pendant
// le chargement du flux).
let cachedSelector = null;
function getLeafItems() {
if (cachedSelector) {
const found = document.querySelectorAll(cachedSelector);
if (found.length > 0) return Array.from(found);
}
for (const sel of LEAF_SELECTORS) {
const found = document.querySelectorAll(sel);
if (found.length > 0) {
cachedSelector = sel;
return Array.from(found);
}
}
return [];
}
// Pour un objet donné, remonte les ancêtres tant qu'ils ne contiennent
// qu'un seul objet (lui-même). Dès qu'un ancêtre englobe un ou plusieurs
// AUTRES objets (cas d'un encart vendeur avec plusieurs pièces), on
// s'arrête au niveau précédent : c'est la "boîte" propre à cet objet.
function findOwnBox(leaf, allLeaves) {
let box = leaf;
let parent = box.parentElement;
while (parent && parent !== document.body) {
const containsOthers = allLeaves.some((other) => other !== leaf && parent.contains(other));
if (containsOthers) break;
box = parent;
parent = parent.parentElement;
}
return box;
}
function getItemBoxes() {
const leaves = getLeafItems();
if (leaves.length === 0) return [];
return leaves.map((leaf) => ({ leaf, box: findOwnBox(leaf, leaves) }));
}
// Texte utilisé pour la recherche de marque : on privilégie l'attribut
// title du lien précis de l'objet (ex: "Nike, prix : 10,00 €, taille : M"),
// qui est fiable même à l'intérieur d'un encart vendeur multi-objets.
function getItemSearchText(leaf) {
const title = leaf.getAttribute && leaf.getAttribute('title');
if (title) return title;
const titledEl = leaf.querySelector ? leaf.querySelector('[title]') : null;
if (titledEl) return titledEl.getAttribute('title') || '';
return leaf.innerText || '';
}
function brandMatches(searchText, brand) {
const escaped = brand.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// recherche du mot/expression avec limites de mots (insensible à la casse)
const re = new RegExp('(^|[^a-zA-Z0-9À-ÖØ-öø-ÿ])' + escaped + '([^a-zA-Z0-9À-ÖØ-öø-ÿ]|$)', 'i');
return re.test(searchText);
}
let hiddenCount = 0;
function filterItems() {
if (excludedBrands.length === 0) {
// réafficher tout si la liste est vide
document.querySelectorAll('[data-vbf-hidden="1"]').forEach((el) => {
el.style.display = '';
el.removeAttribute('data-vbf-hidden');
});
hiddenCount = 0;
updateHiddenCountLabel();
return;
}
const items = getItemBoxes();
hiddenCount = 0;
items.forEach(({ leaf, box }) => {
const searchText = getItemSearchText(leaf);
const matched = excludedBrands.some((brand) => brandMatches(searchText, brand));
if (matched) {
box.style.display = 'none';
box.setAttribute('data-vbf-hidden', '1');
hiddenCount++;
} else if (box.getAttribute('data-vbf-hidden') === '1') {
box.style.display = '';
box.removeAttribute('data-vbf-hidden');
}
});
updateHiddenCountLabel();
}
// Le nombre réel de cartes masquées peut fluctuer très brièvement pendant
// que Vinted charge/remplace des annonces (scroll infini). On n'actualise
// le texte affiché qu'une fois que la valeur est stable pendant un court
// instant, pour éviter que le compteur "clignote" entre deux valeurs.
let labelSettleTimer = null;
let lastShownCount = -1;
function updateHiddenCountLabel() {
clearTimeout(labelSettleTimer);
labelSettleTimer = setTimeout(() => {
if (hiddenCount === lastShownCount) return;
lastShownCount = hiddenCount;
hiddenCountEl.textContent = hiddenCount > 0
? `${hiddenCount} annonce(s) masquée(s) sur cette page`
: 'Aucune annonce masquée sur cette page';
}, 450);
}
// --- Observation du DOM pour le scroll infini / navigation SPA ---
let debounceTimer = null;
function scheduleFilter() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(filterItems, 300);
}
const observer = new MutationObserver(() => scheduleFilter());
observer.observe(document.body, { childList: true, subtree: true });
// Vinted est une SPA : on réagit aussi aux changements d'URL
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
scheduleFilter();
}
}, 800);
// Premier passage
scheduleFilter();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment