// ==UserScript== // @name Remember BrickLink Settings // @namespace https://github.com/pedicino // @version 2.2 // @description Adds a settings wheel to save your BrickLink catalog preferences between sessions. Includes US-based defaults for shipping destination, seller location, and currency. // @author pedicino // @match https://www.bricklink.com/* // @run-at document-start // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; if (typeof GM_addStyle !== 'undefined') { GM_addStyle(` .blp-adv-search__form { width: calc(100% - 48px) !important; transition: none !important; max-width: 100% !important; } .blp-adv-search { width: calc(100% - 48px) !important; transition: none !important; box-sizing: border-box !important; } body .blp-header .blp-adv-search__form { width: calc(100% - 48px) !important; } #bl-preferences-placeholder { width: 40px !important; min-width: 40px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; visibility: hidden !important; } .blp-icon-nav { min-width: 130px !important; display: flex !important; align-items: center !important; justify-content: flex-end !important; } .blp-icon-nav__item-container { min-width: 40px !important; } `); } else { const injectCSS = () => { if (document.head) { const style = document.createElement('style'); style.id = 'bl-preferences-style'; style.textContent = ` .blp-adv-search__form { width: calc(100% - 48px) !important; transition: none !important; } `; document.head.appendChild(style); } else { setTimeout(injectCSS, 5); } }; injectCSS(); } const defaultPrefs = { ss: "US", loc: "US", ca: "1", iconly: 0 }; const PREF_KEY = "BL_preferences"; function savePrefs(prefsToSave) { try { const cleanPrefs = {}; for (const key in prefsToSave) { if (prefsToSave[key] !== undefined) { cleanPrefs[key] = prefsToSave[key]; } } localStorage.setItem(PREF_KEY, JSON.stringify(cleanPrefs)); } catch (e) { console.error("Error saving BL_preferences to localStorage:", e); } } function getPrefs() { let prefsFromStorage = {}; let storedSuccessfully = false; let wasStorageEmpty = true; try { const stored = localStorage.getItem(PREF_KEY); if (stored) { wasStorageEmpty = false; prefsFromStorage = JSON.parse(stored); storedSuccessfully = true; } } catch (e) { console.error(`Error reading/parsing ${PREF_KEY}:`, e); localStorage.removeItem(PREF_KEY); } let effectivePrefs = {}; for (const key in defaultPrefs) { if (prefsFromStorage.hasOwnProperty(key)) { effectivePrefs[key] = prefsFromStorage[key]; } else if (storedSuccessfully) { effectivePrefs[key] = undefined; } else { effectivePrefs[key] = defaultPrefs[key]; } } if (effectivePrefs.iconly === undefined) { effectivePrefs.iconly = defaultPrefs.iconly; } if (wasStorageEmpty || !storedSuccessfully) { savePrefs(effectivePrefs); } return effectivePrefs; } function buildOptionsObject(prefs) { const options = {}; for (const key in prefs) { if (prefs[key] !== undefined && defaultPrefs.hasOwnProperty(key)) { options[key] = prefs[key]; } } options.iconly = prefs.iconly !== undefined ? prefs.iconly : defaultPrefs.iconly; return options; } function buildHashString(options) { const orderedOptions = {}; const order = ['ss', 'loc', 'ca', 'iconly']; order.forEach(key => { if (options.hasOwnProperty(key)) { orderedOptions[key] = options[key]; } }); for(const key in options) { if (!orderedOptions.hasOwnProperty(key)) { orderedOptions[key] = options[key]; } } if (Object.keys(orderedOptions).length === 0) { orderedOptions.iconly = defaultPrefs.iconly; } return "#T=S&O=" + JSON.stringify(orderedOptions); } const isCatalogPage = /\/v2\/catalog\/catalogitem\.page/i.test(window.location.pathname); if (isCatalogPage) { const prefsToEnforce = getPrefs(); const desiredOptions = buildOptionsObject(prefsToEnforce); const desiredHash = buildHashString(desiredOptions); const currentHash = window.location.hash; const decodedHash = decodeURIComponent(currentHash); if (decodedHash !== desiredHash) { let needsRedirect = true; if (decodedHash.startsWith("#T=S&O=")) { try { const currentOptions = JSON.parse(decodedHash.substring(6)); const currentKeys = Object.keys(currentOptions).sort(); const desiredKeys = Object.keys(desiredOptions).sort(); if (currentKeys.length === desiredKeys.length && currentKeys.every((key, index) => key === desiredKeys[index])) { let valuesMatch = true; for (const key of currentKeys) { if (String(currentOptions[key]) !== String(desiredOptions[key])) { valuesMatch = false; break; } } if (valuesMatch) needsRedirect = false; } } catch (e) { } } if (needsRedirect) { const baseUrl = window.location.href.split("#")[0]; const newUrl = baseUrl + desiredHash; console.log(`%cEnforcing preferences via replace...`, "color: blue;"); sessionStorage.setItem("bl_refreshing", "true"); window.location.replace(newUrl); } } } function createPlaceholder() { if (document.getElementById('bl-preferences-placeholder') || document.querySelector('.blp-icon-nav__item-container--preferences')) { return; } const iconNav = document.querySelector(".blp-icon-nav"); if (!iconNav) return; const placeholder = document.createElement("div"); placeholder.id = "bl-preferences-placeholder"; placeholder.className = "blp-icon-nav__item-container"; placeholder.innerHTML = `
`; const mb = iconNav.querySelector(".blp-icon-nav__item-container--more"); if (mb) iconNav.insertBefore(placeholder, mb); else iconNav.appendChild(placeholder); } function createPreferencesUI() { const existingButton = document.querySelector('.blp-icon-nav__item-container--preferences'); const existingPanel = document.getElementById('bl-preferences-panel'); if (existingButton && existingPanel) { initializeCheckboxes(); return; } if (existingButton) existingButton.remove(); if (existingPanel) existingPanel.remove(); const gearSvg = ` `; const navButtonContainer = document.createElement("div"); navButtonContainer.className = "blp-icon-nav__item-container blp-icon-nav__item-container--preferences"; navButtonContainer.style.width = "40px"; navButtonContainer.style.minWidth = "40px"; const navButton = document.createElement("button"); navButton.className = "blp-btn blp-icon-nav__item blp-icon-nav__item--preferences"; navButton.setAttribute("aria-haspopup", "dialog"); navButton.setAttribute("aria-expanded", "false"); navButton.setAttribute("data-state", "closed"); navButton.title = "Marketplace Preferences"; navButton.style.cssText = "background: transparent; transition: all 0.2s; width: 40px;"; navButton.addEventListener("mouseover", () => { const pathElement = iconDiv.querySelector('svg path'); pathElement.setAttribute('stroke', '#0055AA'); }); navButton.addEventListener("mouseout", () => { const pathElement = iconDiv.querySelector('svg path'); pathElement.setAttribute('stroke', '#000000'); }); const iconGroup = document.createElement("div"); iconGroup.className = "blp-icon-nav__item-icon-notification-group"; const iconNotification = document.createElement("span"); iconNotification.className = "blp-icon-nav__item-notification blp-icon-nav__item-notification--hidden"; const iconDiv = document.createElement("div"); iconDiv.className = "blp-icon blp-icon--large"; iconDiv.setAttribute("aria-hidden", "true"); iconDiv.style.cssText = "background: transparent; display: flex; align-items: center; justify-content: center; transform: scale(1.3);"; iconDiv.innerHTML = gearSvg; iconGroup.appendChild(iconNotification); iconGroup.appendChild(iconDiv); navButton.appendChild(iconGroup); navButtonContainer.appendChild(navButton); const placeholder = document.getElementById('bl-preferences-placeholder'); if (placeholder) { placeholder.parentNode.replaceChild(navButtonContainer, placeholder); } else { const iconNav = document.querySelector(".blp-icon-nav"); if (iconNav) { const mb = iconNav.querySelector(".blp-icon-nav__item-container--more"); if (mb) iconNav.insertBefore(navButtonContainer, mb); else iconNav.appendChild(navButtonContainer); } else { const hr = document.querySelector(".blp-header__content"); if (hr) { navButtonContainer.style.cssText = "display:inline-block;margin-right:10px;width:40px;min-width:40px;"; navButton.style.padding = "10px"; const fc = hr.firstChild; if(fc) hr.insertBefore(navButtonContainer, fc); else hr.appendChild(navButtonContainer); } else { console.error("Cannot find insert location"); return; } } } const panel = document.createElement("div"); panel.id = "bl-preferences-panel"; panel.style.cssText = "position: fixed; top: 60px; right: 10px; background: rgba(255,255,255,0.98); border: 1px solid #ccc; padding: 15px; z-index: 10001; font-size: 14px; font-family: Arial, sans-serif; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); display: none;"; panel.innerHTML = `
Marketplace Preferences
`; document.body.appendChild(panel); const ssCheckbox = panel.querySelector("#pref-ss"), locCheckbox = panel.querySelector("#pref-loc"), caCheckbox = panel.querySelector("#pref-ca"), refreshBtn = panel.querySelector("#refreshPrefs"), statusDiv = panel.querySelector("#prefs-status"); let statusTimeout = null; function showStatus(msg, dur=2500, err=false) { if(statusTimeout) clearTimeout(statusTimeout); if(!statusDiv)return; statusDiv.textContent=msg; statusDiv.style.color=err?'red':'green'; if(dur>0){statusTimeout=setTimeout(()=>{if(statusDiv)statusDiv.textContent='';}, dur);} } navButton.addEventListener("click", (e) => { e.stopPropagation(); const disp=panel.style.display==="block"; panel.style.display=disp?"none":"block"; navButton.setAttribute("aria-expanded",!disp); navButton.setAttribute("data-state",disp?"closed":"open"); if(!disp){ initializeCheckboxes(); showStatus('',0); refreshBtn.disabled=false; refreshBtn.style.backgroundColor='#f8f8f8'; refreshBtn.textContent=isCatalogPage?"Apply & Refresh":"Save Preferences";} }); document.addEventListener("click", (e) => { if (panel.style.display==="block" && !panel.contains(e.target) && !navButtonContainer.contains(e.target)) { panel.style.display="none"; navButton.setAttribute("aria-expanded","false"); navButton.setAttribute("data-state","closed"); } }); function initializeCheckboxes() { if (!ssCheckbox || !refreshBtn || panel.style.display==='none') return; const p=getPrefs(); ssCheckbox.checked=(p.ss==="US"); locCheckbox.checked=(p.loc==="US"); caCheckbox.checked=(p.ca==="1"); refreshBtn.textContent=isCatalogPage?"Apply & Refresh":"Save Preferences"; } panel.querySelectorAll("input[type='checkbox']").forEach(input => { input.addEventListener("change", () => { let currentPanelPrefs = { ss: ssCheckbox.checked ? "US" : undefined, loc: locCheckbox.checked ? "US" : undefined, ca: caCheckbox.checked ? "1" : undefined, iconly: 0 }; savePrefs(currentPanelPrefs); showStatus('Preferences updated.', 1500); if (isCatalogPage) { const optionsForHash = buildOptionsObject(currentPanelPrefs); const newHash = buildHashString(optionsForHash); history.replaceState(null, '', window.location.href.split("#")[0] + newHash); console.log("Checkbox change updated hash to:", newHash); } }); }); if (refreshBtn) { refreshBtn.addEventListener("click", function(event) { event.preventDefault(); event.stopPropagation(); const button = this; let desiredPrefs = { ss: ssCheckbox.checked ? "US" : undefined, loc: locCheckbox.checked ? "US" : undefined, ca: caCheckbox.checked ? "1" : undefined, iconly: 0 }; savePrefs(desiredPrefs); button.disabled = true; showStatus('', 0); if (isCatalogPage) { button.textContent = "Reloading..."; button.style.backgroundColor = "#e0e0e0"; console.log("Apply/Refresh clicked. Hash updated by checkbox listener. Attempting reload..."); setTimeout(() => { try { console.log(`%cExecuting window.location.reload()`, "color: green; font-weight: bold;"); window.location.reload(); } catch (err) { console.error("Error during window.location.reload():", err); showStatus('Error reloading page.', 3000, true); button.textContent = "Apply & Refresh"; button.style.backgroundColor = "#f8f8f8"; button.disabled = false; } }, 50); } else { button.textContent = "✓ Saved!"; button.style.backgroundColor = "#d4f8d4"; showStatus('Preferences saved!', 1500); setTimeout(() => { if (panel.style.display === 'block') { button.textContent = "Save Preferences"; button.style.backgroundColor = "#f8f8f8"; button.disabled = false; } }, 1500); } }); } else { console.error("Could not find refresh button."); } initializeCheckboxes(); } const observeDOM = (function(){const M=window.MutationObserver||window.WebKitMutationObserver; return function(o,c){if(!o||o.nodeType!==1)return; if(M){const m=new M(c); m.observe(o,{childList:true,subtree:true}); return m;}else{o.addEventListener('DOMNodeInserted',c,false);o.addEventListener('DOMNodeRemoved',c,false);return null;}}})(); function tryEarlyPlaceholder() { if (document.body) { createPlaceholder(); } else { setTimeout(tryEarlyPlaceholder, 5); } } tryEarlyPlaceholder(); function checkAndCreateUI() { createPlaceholder(); const h=document.querySelector(".blp-header")||document.body; const b=document.querySelector(".blp-icon-nav__item-container--preferences"); const p=document.getElementById("bl-preferences-panel"); if(h&&(!b||!p)){createPreferencesUI();}else if(h&&b&&p){ if(p.style.display==='block'){initializeCheckboxes();} } } function initialSetup() { if(document.body){ createPlaceholder(); setTimeout(() => { const forms = document.querySelectorAll('.blp-adv-search__form'); for (const form of forms) { form.style.display = 'none'; void form.offsetWidth; form.style.display = ''; } }, 0); if(sessionStorage.getItem("bl_refreshing")==="true"){ sessionStorage.removeItem("bl_refreshing"); setTimeout(checkAndCreateUI, 50); } else { setTimeout(checkAndCreateUI, 0); } observeDOM(document.body, function(m){ let pc=false; for(const mut of m){ if(mut.type==='childList'){ createPlaceholder(); for (const node of mut.addedNodes) { if (node.nodeType === 1 && (node.classList?.contains('blp-adv-search__form') || node.querySelector?.('.blp-adv-search__form'))) { pc = true; break; } } if(mut.target.closest('.blp-header')){pc=true;break;} for(const n of mut.removedNodes){ if(n.nodeType===1){ if(n.querySelector?.('.blp-icon-nav__item-container--preferences') || n.id==='bl-preferences-panel' || n.classList?.contains('blp-icon-nav__item-container--preferences')){ pc=true;break; } } } if(pc)break; } } if(pc){setTimeout(checkAndCreateUI, 0);} }); } else { setTimeout(initialSetup, 5); } } if(document.readyState==="loading"){ document.addEventListener("DOMContentLoaded", initialSetup); } else { initialSetup(); } })();