Created
March 14, 2026 18:12
-
-
Save playday3008/23beff9299918bf1af48dbf6e06ce0a3 to your computer and use it in GitHub Desktop.
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 Domino's Pizza Ingredient Filter | |
| // @namespace https://www.dominospizza.pl | |
| // @match https://www.dominospizza.pl/* | |
| // @grant none | |
| // @version 1.1 | |
| // @author PlayDay | |
| // @description Whitelist/blacklist ingredients and filter by tags on Domino's Pizza Poland menu | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const STORAGE_KEY = 'dominos-ingredient-filter'; | |
| const TAGS_KEY = 'dominos-tag-filter'; | |
| function loadPrefs() { | |
| try { | |
| return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); | |
| } catch { return {}; } | |
| } | |
| function loadTagPrefs() { | |
| try { | |
| return JSON.parse(localStorage.getItem(TAGS_KEY) || '{}'); | |
| } catch { return {}; } | |
| } | |
| function savePrefs(prefs) { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs)); | |
| } | |
| function saveTagPrefs(prefs) { | |
| localStorage.setItem(TAGS_KEY, JSON.stringify(prefs)); | |
| } | |
| function normalizeIngredient(raw) { | |
| return raw.replace(/\u00a0/g, ' ').trim().toLowerCase(); | |
| } | |
| function extractIngredients(descEl) { | |
| return descEl.textContent | |
| .split(',') | |
| .map(normalizeIngredient) | |
| .filter(Boolean); | |
| } | |
| function extractTags(item) { | |
| const tags = []; | |
| item.querySelectorAll('.m-AppProductItem__infoTags-text').forEach(el => { | |
| const t = el.textContent.trim(); | |
| if (t) tags.push(t); | |
| }); | |
| return tags; | |
| } | |
| function getAllIngredients() { | |
| const set = new Set(); | |
| document.querySelectorAll('.m-AppProductItem__infoDescription').forEach(el => { | |
| extractIngredients(el).forEach(i => set.add(i)); | |
| }); | |
| return [...set].sort((a, b) => a.localeCompare(b, 'pl')); | |
| } | |
| function getAllTags() { | |
| const set = new Set(); | |
| document.querySelectorAll('.m-AppProductItem__infoTags-text').forEach(el => { | |
| const t = el.textContent.trim(); | |
| if (t) set.add(t); | |
| }); | |
| return [...set].sort((a, b) => a.localeCompare(b, 'pl')); | |
| } | |
| const COLORS = { want: '#2e7d32', ban: '#c62828' }; | |
| const BGS = { want: '#e8f5e9', ban: '#ffebee' }; | |
| function applyFilter(prefs, tagPrefs) { | |
| const items = document.querySelectorAll('.m-AppProductItem'); | |
| const hasIngredientWhitelist = Object.values(prefs).includes('want'); | |
| const hasTagWhitelist = Object.values(tagPrefs).includes('want'); | |
| items.forEach(item => { | |
| const descEl = item.querySelector('.m-AppProductItem__infoDescription'); | |
| if (!descEl) return; | |
| const ingredients = extractIngredients(descEl); | |
| const tags = extractTags(item); | |
| // Tag filter — "want" tags override ingredient filter entirely | |
| const hasWantedTag = tags.some(t => tagPrefs[t] === 'want'); | |
| const hasBannedTag = tags.some(t => tagPrefs[t] === 'ban'); | |
| let hidden = false; | |
| if (hasWantedTag) { | |
| // Wanted tag = always show, skip ingredient logic | |
| hidden = false; | |
| } else if (hasBannedTag) { | |
| hidden = true; | |
| } else { | |
| // Ingredient-level filtering | |
| const hasBannedIng = ingredients.some(i => prefs[i] === 'ban'); | |
| const hasWantedIng = ingredients.some(i => prefs[i] === 'want'); | |
| if (hasBannedIng) hidden = true; | |
| if (hasIngredientWhitelist && !hasWantedIng) hidden = true; | |
| // If tag whitelist is active but this pizza has no wanted tag, dim it too | |
| if (hasTagWhitelist) hidden = true; | |
| } | |
| item.style.opacity = hidden ? '0.15' : '1'; | |
| item.style.order = hidden ? '999' : ''; | |
| // Highlight matching ingredients in description | |
| const parts = descEl.textContent.split(',').map(s => s.replace(/\u00a0/g, ' ').trim()); | |
| descEl.innerHTML = parts.map(p => { | |
| const norm = p.toLowerCase(); | |
| const state = prefs[norm] || null; | |
| if (state === 'want') return `<span style="color:#2e7d32;font-weight:bold">${p}</span>`; | |
| if (state === 'ban') return `<span style="color:#c62828;text-decoration:line-through">${p}</span>`; | |
| return p; | |
| }).join(', '); | |
| }); | |
| } | |
| function createPanel() { | |
| const prefs = loadPrefs(); | |
| const tagPrefs = loadTagPrefs(); | |
| const ingredients = getAllIngredients(); | |
| const tags = getAllTags(); | |
| const panel = document.createElement('div'); | |
| panel.id = 'dominos-filter-panel'; | |
| Object.assign(panel.style, { | |
| position: 'fixed', top: '10px', right: '10px', zIndex: '999999', | |
| background: '#fff', border: '2px solid #0b6ab0', borderRadius: '10px', | |
| padding: '0', maxHeight: '90vh', width: '320px', | |
| boxShadow: '0 4px 20px rgba(0,0,0,0.3)', fontFamily: 'Arial, sans-serif', | |
| display: 'flex', flexDirection: 'column', overflow: 'hidden' | |
| }); | |
| // Header | |
| const header = document.createElement('div'); | |
| Object.assign(header.style, { | |
| padding: '10px 14px', background: '#0b6ab0', color: '#fff', | |
| fontWeight: 'bold', fontSize: '14px', cursor: 'pointer', | |
| display: 'flex', justifyContent: 'space-between', alignItems: 'center', | |
| userSelect: 'none' | |
| }); | |
| header.innerHTML = '<span>Pizza Filter</span><span id="df-toggle">_</span>'; | |
| panel.appendChild(header); | |
| // Body | |
| const body = document.createElement('div'); | |
| body.id = 'df-body'; | |
| Object.assign(body.style, { | |
| padding: '10px 14px', overflowY: 'auto', flex: '1' | |
| }); | |
| // Reset button | |
| const resetBtn = document.createElement('button'); | |
| resetBtn.textContent = 'Reset All'; | |
| Object.assign(resetBtn.style, { | |
| width: '100%', padding: '6px', marginBottom: '10px', cursor: 'pointer', | |
| border: '1px solid #ccc', borderRadius: '4px', background: '#f5f5f5', | |
| fontSize: '12px' | |
| }); | |
| resetBtn.addEventListener('click', () => { | |
| savePrefs({}); | |
| saveTagPrefs({}); | |
| panel.remove(); | |
| createPanel(); | |
| }); | |
| body.appendChild(resetBtn); | |
| function makeToggleBtn(key, state, text, color, bg, currentPrefs, saveFn) { | |
| const btn = document.createElement('button'); | |
| btn.textContent = text; | |
| const isActive = currentPrefs[key] === state; | |
| Object.assign(btn.style, { | |
| padding: '2px 8px', fontSize: '11px', cursor: 'pointer', | |
| border: `1px solid ${color}`, borderRadius: '3px', | |
| background: isActive ? bg : '#fff', | |
| color: isActive ? color : '#aaa', | |
| fontWeight: isActive ? 'bold' : 'normal' | |
| }); | |
| btn.dataset.state = state; | |
| btn.addEventListener('click', () => { | |
| const current = saveFn === savePrefs ? loadPrefs() : loadTagPrefs(); | |
| if (current[key] === state) { | |
| delete current[key]; | |
| } else { | |
| current[key] = state; | |
| } | |
| saveFn(current); | |
| applyFilter( | |
| saveFn === savePrefs ? current : loadPrefs(), | |
| saveFn === saveTagPrefs ? current : loadTagPrefs() | |
| ); | |
| // Update sibling buttons | |
| btn.parentElement.querySelectorAll('button').forEach(b => { | |
| const bActive = current[key] === b.dataset.state; | |
| const bColor = b.dataset.state === 'want' ? COLORS.want : COLORS.ban; | |
| const bBg = b.dataset.state === 'want' ? BGS.want : BGS.ban; | |
| b.style.background = bActive ? bBg : '#fff'; | |
| b.style.color = bActive ? bColor : '#aaa'; | |
| b.style.fontWeight = bActive ? 'bold' : 'normal'; | |
| }); | |
| }); | |
| return btn; | |
| } | |
| function makeRow(key, label, currentPrefs, saveFn) { | |
| const row = document.createElement('div'); | |
| Object.assign(row.style, { | |
| display: 'flex', alignItems: 'center', justifyContent: 'space-between', | |
| padding: '4px 0', borderBottom: '1px solid #eee' | |
| }); | |
| const labelEl = document.createElement('span'); | |
| labelEl.textContent = label; | |
| Object.assign(labelEl.style, { | |
| fontSize: '13px', flex: '1', marginRight: '8px' | |
| }); | |
| const btnGroup = document.createElement('div'); | |
| Object.assign(btnGroup.style, { display: 'flex', gap: '4px' }); | |
| btnGroup.appendChild(makeToggleBtn(key, 'want', 'Want', '#2e7d32', '#e8f5e9', currentPrefs, saveFn)); | |
| btnGroup.appendChild(makeToggleBtn(key, 'ban', 'Ban', '#c62828', '#ffebee', currentPrefs, saveFn)); | |
| row.appendChild(labelEl); | |
| row.appendChild(btnGroup); | |
| return row; | |
| } | |
| function makeSectionHeader(text) { | |
| const h = document.createElement('div'); | |
| Object.assign(h.style, { | |
| fontSize: '12px', fontWeight: 'bold', color: '#0b6ab0', | |
| padding: '8px 0 4px', borderBottom: '2px solid #0b6ab0', | |
| marginTop: '6px', textTransform: 'uppercase', letterSpacing: '0.5px' | |
| }); | |
| h.textContent = text; | |
| return h; | |
| } | |
| // Tags section | |
| if (tags.length > 0) { | |
| body.appendChild(makeSectionHeader('Tags')); | |
| tags.forEach(tag => { | |
| body.appendChild(makeRow(tag, tag, tagPrefs, saveTagPrefs)); | |
| }); | |
| } | |
| // Ingredients section | |
| body.appendChild(makeSectionHeader('Ingredients')); | |
| ingredients.forEach(ing => { | |
| const row = makeRow(ing, ing, prefs, savePrefs); | |
| row.querySelector('span').style.textTransform = 'capitalize'; | |
| body.appendChild(row); | |
| }); | |
| panel.appendChild(body); | |
| // Legend | |
| const legend = document.createElement('div'); | |
| Object.assign(legend.style, { | |
| padding: '8px 14px', background: '#f5f5f5', fontSize: '11px', | |
| color: '#666', borderTop: '1px solid #ddd' | |
| }); | |
| legend.innerHTML = '<b>Want</b> tag overrides ingredient filter<br><b>Ban</b>: hide pizzas | <b>Want</b>: show only matching'; | |
| panel.appendChild(legend); | |
| // Toggle collapse | |
| let collapsed = false; | |
| header.addEventListener('click', () => { | |
| collapsed = !collapsed; | |
| body.style.display = collapsed ? 'none' : 'block'; | |
| legend.style.display = collapsed ? 'none' : 'block'; | |
| const toggle = document.getElementById('df-toggle'); | |
| if (toggle) toggle.textContent = collapsed ? '+' : '_'; | |
| }); | |
| document.body.appendChild(panel); | |
| applyFilter(prefs, tagPrefs); | |
| } | |
| // Wait for menu to load, then init | |
| function init() { | |
| if (document.querySelector('.m-AppProductItem__infoDescription')) { | |
| createPanel(); | |
| } else { | |
| const observer = new MutationObserver(() => { | |
| if (document.querySelector('.m-AppProductItem__infoDescription')) { | |
| observer.disconnect(); | |
| createPanel(); | |
| } | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| init(); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment