Last active
July 19, 2026 20:28
-
-
Save konfiot/0e756989ceb9257afdbe542e64e45694 to your computer and use it in GitHub Desktop.
Vinted filtre
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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