nice clean recipes pear.dunkirk.sh
recipes
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add userscript

+128 -3
+122
ui/static/pear.user.js
··· 1 + // ==UserScript== 2 + // @name Pear 3 + // @namespace https://pear.dunkirk.sh 4 + // @version 1.5 5 + // @description Detect recipe pages and offer to clean them with Pear 6 + // @author you 7 + // @match *://*/* 8 + // @grant none 9 + // @run-at document-idle 10 + // @downloadURL https://pear.dunkirk.sh/static/pear.user.js 11 + // @updateURL https://pear.dunkirk.sh/static/pear.user.js 12 + // ==/UserScript== 13 + 14 + (function () { 15 + const PEAR_URL = "https://pear.dunkirk.sh"; 16 + 17 + function findRecipe(v) { 18 + if (typeof v !== "object" || v === null) return false; 19 + if (Array.isArray(v)) return v.some(findRecipe); 20 + const types = [v["@type"] ?? []].flat(); 21 + if (types.some((t) => (typeof t === "string" ? t : "").includes("Recipe"))) return true; 22 + if (v["@graph"] && findRecipe(v["@graph"])) return true; 23 + for (const val of Object.values(v)) { 24 + if (typeof val === "object" && val !== null && findRecipe(val)) return true; 25 + } 26 + return false; 27 + } 28 + 29 + function hasRecipeLD() { 30 + for (const el of document.querySelectorAll('script[type="application/ld+json"]')) { 31 + try { 32 + if (findRecipe(JSON.parse(el.textContent))) return true; 33 + } catch {} 34 + } 35 + return false; 36 + } 37 + 38 + if (!hasRecipeLD()) return; 39 + 40 + const BAR_H = 50; 41 + const bar = document.createElement("div"); 42 + bar.id = "pear-bar"; 43 + bar.innerHTML = ` 44 + <span id="pear-label">I found a recipe! Do you wish to</span> 45 + <a href="${PEAR_URL}/?url=${encodeURIComponent(window.location.href)}" id="pear-link">open it in Pear?</a> 46 + <button id="pear-close" title="Dismiss"> 47 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg> 48 + </button> 49 + `; 50 + 51 + const style = document.createElement("style"); 52 + style.textContent = ` 53 + html { margin-top: ${BAR_H}px !important; } 54 + #pear-bar { 55 + position: fixed; 56 + top: 0; left: 0; right: 0; 57 + z-index: 2147483647; 58 + display: flex; 59 + align-items: center; 60 + justify-content: center; 61 + gap: 8px; 62 + height: ${BAR_H}px; 63 + padding: 0 40px 0 16px; 64 + background: #1a1a2e; 65 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; 66 + box-shadow: 0 2px 12px rgba(0,0,0,0.2); 67 + box-sizing: border-box; 68 + } 69 + #pear-label { 70 + color: rgba(255,255,255,0.5); 71 + font-size: 13px; 72 + font-weight: 400; 73 + } 74 + #pear-link { 75 + color: #e85d04; 76 + font-size: 13px; 77 + font-weight: 600; 78 + text-decoration: none; 79 + transition: color 0.15s; 80 + } 81 + #pear-link:hover { color: #ff7b2e; text-decoration: underline; } 82 + #pear-close { 83 + position: absolute; 84 + right: 10px; 85 + top: 50%; 86 + transform: translateY(-50%); 87 + background: none; 88 + border: none; 89 + color: rgba(255,255,255,0.3); 90 + cursor: pointer; 91 + padding: 6px; 92 + border-radius: 4px; 93 + display: flex; 94 + align-items: center; 95 + justify-content: center; 96 + transition: color 0.15s, background 0.15s; 97 + } 98 + #pear-close:hover { color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.08); } 99 + `; 100 + document.head.appendChild(style); 101 + document.body.appendChild(bar); 102 + 103 + const shifted = []; 104 + document.querySelectorAll("*").forEach((el) => { 105 + if (el.id === "pear-bar" || el.closest("#pear-bar")) return; 106 + const cs = getComputedStyle(el); 107 + if (cs.position === "fixed" || cs.position === "sticky") { 108 + const orig = el.getAttribute("style") || ""; 109 + const current = parseFloat(cs.top) || 0; 110 + el.style.setProperty("top", (current + BAR_H) + "px", "important"); 111 + shifted.push({ el, orig }); 112 + } 113 + }); 114 + 115 + document.getElementById("pear-close").addEventListener("click", () => { 116 + bar.remove(); 117 + style.remove(); 118 + shifted.forEach(({ el, orig }) => { 119 + if (orig) { el.setAttribute("style", orig); } else { el.removeAttribute("style"); } 120 + }); 121 + }); 122 + })();
+5 -3
ui/static/style.css
··· 80 80 letter-spacing:-0.03125em; 81 81 font-weight:700; 82 82 } 83 - .hero{text-align:center;padding:4rem 0 2.5rem} 84 - .hero p{color:var(--text-muted);font-size:1.05rem;margin-bottom:2rem} 83 + .hero{text-align:center;padding:2.5rem 0 1rem} 84 + .hero p{color:var(--text-muted);font-size:1.05rem;margin-bottom:0.5rem} 85 + .userscript-hint{color:var(--text-muted);font-size:0.85rem;margin-top:0.5rem;font-family:'Poppins',system-ui,sans-serif} 86 + .userscript-hint a{color:var(--accent)} 85 87 86 88 .search-form{} 87 89 .search-form form{display:flex;gap:0.5rem} ··· 485 487 .cook-code .ck-qty{color:#b5cea8} 486 488 .cook-code .ck-unit{color:#ce9178} 487 489 488 - .recent-recipes{margin-top:3rem} 490 + .recent-recipes{margin-top:1.5rem} 489 491 .recent-recipes h3{ 490 492 font-size:0.85rem; 491 493 text-transform:uppercase;
+1
ui/templates/index.html
··· 28 28 <button type="submit">pear it</button> 29 29 </form> 30 30 </div> 31 + <p class="userscript-hint">or <a href="/static/pear.user.js">install the userscript</a> to auto-detect recipes</p> 31 32 </div> 32 33 {{if .Recent}} 33 34 <div class="recent-recipes">