// ==UserScript== // @name Masonry Image Gallery // @namespace https://github.com/kouxing2000/tampermonkey-scripts // @version 1.3.0 // @author kouxing2000 // @description View all images on any page as a smooth Pinterest-style masonry wall, with lightbox + keyboard nav. Download single images, or select many and save them individually or as a zip. Toggle with Ctrl+Alt+G (rebindable in-app). // @license MIT // @downloadURL https://raw.githubusercontent.com/kouxing2000/tampermonkey-scripts/main/scripts/masonry-image-gallery/dist/masonry-image-gallery.user.js // @updateURL https://raw.githubusercontent.com/kouxing2000/tampermonkey-scripts/main/scripts/masonry-image-gallery/dist/masonry-image-gallery.user.js // @match *://*/* // @connect * // @grant GM_download // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_xmlhttpRequest // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; const SHORTCUT = { ctrl: true, alt: true, shift: false, meta: false, code: "KeyG" }; const MIN_SIZE = 80; const DEFAULT_COL_WIDTH = 240; const GAP = 12; const DOWNLOAD_CONCURRENCY = 5; const ZIP_FILENAME = "images"; function bestFromSrcset(srcset) { if (!srcset) return null; let best = null; let bestScore = -1; for (const part of srcset.split(",")) { const tokens = part.trim().split(/\s+/); const url = tokens[0]; if (!url) continue; let score = 1; const d = tokens[1]; if (d) { if (d.endsWith("w")) score = parseInt(d, 10) || 1; else if (d.endsWith("x")) score = (parseFloat(d) || 1) * 1e3; } if (score > bestScore) { bestScore = score; best = url; } } return best; } function parseBackgroundImageUrls(bg) { if (!bg || bg === "none") return []; const out = []; const re = /url\((['"]?)(.*?)\1\)/g; let m; while ((m = re.exec(bg)) !== null) { if (m[2]) out.push(m[2]); } return out; } function resolveUrl(url, base) { try { return new URL(url, base).href; } catch (e) { return null; } } function isSkippableDataUri(url) { return /^data:image\/(gif|svg)/i.test(url || ""); } function shortcutLabel(shortcut, isMac2) { const parts = []; if (shortcut.ctrl) parts.push(isMac2 ? "Control" : "Ctrl"); if (shortcut.alt) parts.push(isMac2 ? "Option" : "Alt"); if (shortcut.shift) parts.push("Shift"); if (shortcut.meta) parts.push(isMac2 ? "Command" : "Win"); parts.push((shortcut.code || "").replace(/^Key|^Digit/, "")); return parts.join("+"); } function matchesShortcut(e, shortcut) { return e.ctrlKey === !!shortcut.ctrl && e.altKey === !!shortcut.alt && e.shiftKey === !!shortcut.shift && e.metaKey === !!shortcut.meta && e.code === shortcut.code; } const MODIFIER_CODES = /* @__PURE__ */ new Set([ "ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight", "MetaLeft", "MetaRight", "OSLeft", "OSRight" ]); function isModifierKey(code) { return MODIFIER_CODES.has(code); } function shortcutFromEvent(e) { return { ctrl: !!e.ctrlKey, alt: !!e.altKey, shift: !!e.shiftKey, meta: !!e.metaKey, code: e.code }; } function isValidShortcut(s) { if (!s || typeof s.code !== "string" || !s.code) return false; if (isModifierKey(s.code)) return false; const hasModifier = !!(s.ctrl || s.alt || s.meta); const isFunctionKey = /^F([1-9]|1[0-9]|2[0-4])$/.test(s.code); return hasModifier || isFunctionKey; } function filenameFromUrl(url, index = 0) { const ordinal = String(index + 1).padStart(3, "0"); let name = ""; try { const u = new URL(url); if (u.protocol === "data:") { const m = /^data:image\/([a-z0-9.+-]+)/i.exec(url); const ext = m ? m[1].toLowerCase().replace("jpeg", "jpg").replace(/[^a-z0-9]/g, "") : "png"; return `image-${ordinal}.${ext || "png"}`; } name = decodeURIComponent(u.pathname.split("/").pop() || ""); } catch (e) { name = (url || "").split(/[?#]/)[0].split("/").pop() || ""; } name = name.replace(/[^\w.\-]+/g, "_").replace(/^[_.]+|_+$/g, ""); if (!/\.[a-z0-9]{2,5}$/i.test(name)) { const stem = name.replace(/\.+$/, "") || `image-${ordinal}`; name = `${stem}.jpg`; } return name; } function uniquifyNames(names) { const seen = /* @__PURE__ */ new Map(); return names.map((raw) => { const key = raw.toLowerCase(); const n = seen.get(key) || 0; seen.set(key, n + 1); if (n === 0) return raw; const dot = raw.lastIndexOf("."); const stem = dot > 0 ? raw.slice(0, dot) : raw; const ext = dot > 0 ? raw.slice(dot) : ""; return `${stem}-${n}${ext}`; }); } let CRC_TABLE = null; function crcTable() { if (CRC_TABLE) return CRC_TABLE; const t = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1; t[n] = c >>> 0; } CRC_TABLE = t; return t; } function crc32(bytes) { const t = crcTable(); let c = 4294967295; for (let i = 0; i < bytes.length; i++) c = t[(c ^ bytes[i]) & 255] ^ c >>> 8; return (c ^ 4294967295) >>> 0; } const DOS_TIME = 0; const DOS_DATE = 33; function buildZip(files) { const enc = new TextEncoder(); const parts = []; const central = []; let offset = 0; for (const f of files) { const nameBytes = enc.encode(f.name); const data = f.bytes; const crc = crc32(data); const size = data.length; const local = new Uint8Array(30 + nameBytes.length); const lv = new DataView(local.buffer); lv.setUint32(0, 67324752, true); lv.setUint16(4, 20, true); lv.setUint16(6, 2048, true); lv.setUint16(8, 0, true); lv.setUint16(10, DOS_TIME, true); lv.setUint16(12, DOS_DATE, true); lv.setUint32(14, crc, true); lv.setUint32(18, size, true); lv.setUint32(22, size, true); lv.setUint16(26, nameBytes.length, true); lv.setUint16(28, 0, true); local.set(nameBytes, 30); parts.push(local, data); const cen = new Uint8Array(46 + nameBytes.length); const cv = new DataView(cen.buffer); cv.setUint32(0, 33639248, true); cv.setUint16(4, 20, true); cv.setUint16(6, 20, true); cv.setUint16(8, 2048, true); cv.setUint16(10, 0, true); cv.setUint16(12, DOS_TIME, true); cv.setUint16(14, DOS_DATE, true); cv.setUint32(16, crc, true); cv.setUint32(20, size, true); cv.setUint32(24, size, true); cv.setUint16(28, nameBytes.length, true); cv.setUint16(30, 0, true); cv.setUint16(32, 0, true); cv.setUint16(34, 0, true); cv.setUint16(36, 0, true); cv.setUint32(38, 0, true); cv.setUint32(42, offset, true); cen.set(nameBytes, 46); central.push(cen); offset += local.length + data.length; } const centralSize = central.reduce((n, c) => n + c.length, 0); const centralOffset = offset; const end = new Uint8Array(22); const ev = new DataView(end.buffer); ev.setUint32(0, 101010256, true); ev.setUint16(4, 0, true); ev.setUint16(6, 0, true); ev.setUint16(8, files.length, true); ev.setUint16(10, files.length, true); ev.setUint32(12, centralSize, true); ev.setUint32(16, centralOffset, true); ev.setUint16(20, 0, true); const all = parts.concat(central, [end]); const total = all.reduce((n, c) => n + c.length, 0); const out = new Uint8Array(total); let p = 0; for (const c of all) { out.set(c, p); p += c.length; } return out; } const DL_ICON = ''; const Z = 2147483600; const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform); const store = { get(k, d) { try { if (typeof GM_getValue === "function") return GM_getValue(k, d); const raw = localStorage.getItem("mig_" + k); return raw == null ? d : JSON.parse(raw) ?? d; } catch (e) { return d; } }, set(k, v) { try { if (typeof GM_setValue === "function") GM_setValue(k, v); else localStorage.setItem("mig_" + k, JSON.stringify(v)); } catch (e) { } } }; let open = false; let images = []; let lightboxIndex = -1; let root, grid, scroller, lb, lbImg, lbCounter; let launchBtn = null; let capturing = false; let busy = false; const selected = /* @__PURE__ */ new Set(); function currentShortcut() { const saved = store.get("shortcut", null); return saved && isValidShortcut(saved) ? saved : SHORTCUT; } function collect() { const seen = /* @__PURE__ */ new Set(); const out = []; const isOurs = (el) => el.closest && el.closest(".mig-root, .mig-launch"); const push = (rawUrl, w, h) => { if (!rawUrl) return; if (isSkippableDataUri(rawUrl)) return; const url = resolveUrl(rawUrl, location.href); if (!url || seen.has(url)) return; seen.add(url); out.push({ url, w: w || 0, h: h || 0 }); }; document.querySelectorAll("img").forEach((img) => { if (isOurs(img)) return; const nat = { w: img.naturalWidth, h: img.naturalHeight }; const rect = img.getBoundingClientRect(); const w = nat.w || rect.width; const h = nat.h || rect.height; if (w && h && (w < MIN_SIZE || h < MIN_SIZE)) { if (nat.w && nat.h) return; } const candidate = img.currentSrc || img.srcset && bestFromSrcset(img.srcset) || img.getAttribute("data-src") || img.getAttribute("data-original") || img.getAttribute("data-lazy-src") || img.getAttribute("data-srcset") && bestFromSrcset(img.getAttribute("data-srcset")) || img.src; push(candidate, w, h); }); document.querySelectorAll("source[srcset]").forEach((s) => { if (isOurs(s)) return; push(bestFromSrcset(s.srcset)); }); document.querySelectorAll("*").forEach((el) => { if (isOurs(el)) return; const urls = parseBackgroundImageUrls(getComputedStyle(el).backgroundImage); if (!urls.length) return; const rect = el.getBoundingClientRect(); if (rect.width < MIN_SIZE || rect.height < MIN_SIZE) return; urls.forEach((u) => push(u, rect.width, rect.height)); }); return out; } function fetchBytes(url) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest({ method: "GET", url, responseType: "arraybuffer", onload: (res) => { if (res.status >= 200 && res.status < 300 && res.response) { resolve(new Uint8Array(res.response)); } else { reject(new Error("HTTP " + res.status)); } }, onerror: () => reject(new Error("network error")), ontimeout: () => reject(new Error("timeout")) }); return; } fetch(url).then((r) => r.ok ? r.arrayBuffer() : Promise.reject(new Error("HTTP " + r.status))).then((b) => resolve(new Uint8Array(b))).catch(reject); }); } function saveBlob(blob, name) { const u = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = u; a.download = name; a.style.display = "none"; document.documentElement.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(u), 1e4); } function downloadOne(url, index) { const name = filenameFromUrl(url, index); if (typeof GM_download === "function") { try { GM_download({ url, name, onerror: () => saveViaFetch(url, name) }); return; } catch (e) { } } saveViaFetch(url, name); } async function saveViaFetch(url, name) { try { const bytes = await fetchBytes(url); saveBlob(new Blob([bytes]), name); } catch (e) { window.open(url, "_blank", "noopener"); } } async function runPool(items, limit, worker, onProgress) { const results = new Array(items.length); let next = 0; let done = 0; async function lane() { while (next < items.length) { const i = next++; results[i] = await worker(items[i], i); done++; if (onProgress) onProgress(done, items.length); } } const lanes = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, lane); await Promise.all(lanes); return results; } async function downloadZip(entries, onProgress) { const fetched = await runPool( entries, DOWNLOAD_CONCURRENCY, async (entry) => { try { return { ...entry, bytes: await fetchBytes(entry.url) }; } catch (e) { return null; } }, onProgress ); const ok = fetched.filter(Boolean); if (!ok.length) return { saved: 0, failed: entries.length }; const names = uniquifyNames(ok.map((r) => filenameFromUrl(r.url, r.index))); const zip = buildZip(ok.map((r, k) => ({ name: names[k], bytes: r.bytes }))); saveBlob(new Blob([zip], { type: "application/zip" }), ZIP_FILENAME + ".zip"); return { saved: ok.length, failed: entries.length - ok.length }; } function css() { const colW = store.get("colWidth", DEFAULT_COL_WIDTH); return ` .mig-root{position:fixed;inset:0;z-index:${Z};background:#0d0d0f;color:#e8e8ea; font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; display:flex;flex-direction:column;} .mig-bar{flex:0 0 auto;display:flex;align-items:center;gap:16px;padding:10px 16px; background:#16161a;border-bottom:1px solid #26262c;} .mig-bar .mig-title{font-weight:600;letter-spacing:.2px;} .mig-bar .mig-count{color:#9a9aa2;} .mig-bar .mig-spacer{flex:1;} .mig-bar label{display:flex;align-items:center;gap:8px;color:#b8b8c0;} .mig-bar input[type=range]{accent-color:#7c6cff;} .mig-bar button{background:#26262c;color:#e8e8ea;border:1px solid #34343c;border-radius:8px; padding:6px 12px;cursor:pointer;font-size:13px;} .mig-bar button:hover{background:#34343c;} .mig-bar button:disabled{opacity:.4;cursor:default;} .mig-bar .mig-sep{width:1px;align-self:stretch;background:#2c2c34;margin:0 2px;} .mig-bar .mig-selcount{color:#9a9aa2;min-width:74px;} .mig-bar .mig-dlbtn{display:inline-flex;align-items:center;gap:6px;} .mig-bar .mig-dlbtn svg{width:14px;height:14px;} .mig-bar .mig-status{color:#b8b8c0;font-size:12px;} .mig-bar label.mig-selall{cursor:pointer;} .mig-scroll{flex:1 1 auto;overflow-y:auto;overflow-x:hidden;padding:${GAP}px;} .mig-grid{column-width:${colW}px;column-gap:${GAP}px;column-fill:balance;} .mig-tile{position:relative;break-inside:avoid;margin:0 0 ${GAP}px;border-radius:10px;overflow:hidden; background:#1a1a1f;cursor:zoom-in;display:block;line-height:0; border:1px solid #222228;transition:transform .12s ease,border-color .12s ease;} .mig-tile:hover{transform:translateY(-2px);border-color:#7c6cff;} .mig-tile.mig-sel{border-color:#7c6cff;box-shadow:0 0 0 2px #7c6cff inset;} .mig-tile img{width:100%;height:auto;display:block;} .mig-tile-ctl{position:absolute;top:8px;display:flex;align-items:center;justify-content:center; opacity:0;transition:opacity .12s ease;z-index:2;line-height:1;} .mig-tile:hover .mig-tile-ctl,.mig-tile.mig-sel .mig-check{opacity:1;} .mig-check{left:8px;width:20px;height:20px;margin:0;cursor:pointer;accent-color:#7c6cff; border-radius:4px;} .mig-dl{right:8px;width:30px;height:30px;border:none;border-radius:8px;cursor:pointer; color:#fff;background:rgba(20,20,24,.78);box-shadow:0 2px 8px rgba(0,0,0,.4);padding:0;} .mig-dl:hover{background:#7c6cff;} .mig-dl svg{width:16px;height:16px;} .mig-lb{position:fixed;inset:0;z-index:${Z + 1};background:rgba(8,8,10,.94); display:none;align-items:center;justify-content:center;} .mig-lb.open{display:flex;} .mig-lb img{max-width:92vw;max-height:88vh;object-fit:contain;border-radius:6px; box-shadow:0 12px 60px rgba(0,0,0,.6);} .mig-lb-nav{position:absolute;top:64px;bottom:0;width:16vw;display:flex;align-items:center; cursor:pointer;color:#fff;font-size:40px;opacity:.4;user-select:none;z-index:2;} .mig-lb-nav:hover{opacity:1;} .mig-lb-prev{left:0;justify-content:flex-start;padding-left:20px;} .mig-lb-next{right:0;justify-content:flex-end;padding-right:20px;} .mig-lb-counter{position:absolute;top:18px;left:50%;transform:translateX(-50%); background:rgba(0,0,0,.5);padding:6px 14px;border-radius:20px;font-size:13px;z-index:4;} .mig-lb-close{position:absolute;top:10px;right:16px;font-size:34px;line-height:1;cursor:pointer; color:#fff;opacity:.75;width:44px;height:44px;display:flex;align-items:center; justify-content:center;z-index:4;} .mig-lb-close:hover{opacity:1;} .mig-lb-dl{position:absolute;top:16px;right:70px;width:40px;height:40px;z-index:4;cursor:pointer; color:#fff;background:rgba(20,20,24,.7);border:none;border-radius:10px;opacity:.85; display:flex;align-items:center;justify-content:center;} .mig-lb-dl:hover{opacity:1;background:#7c6cff;} .mig-lb-dl svg{width:20px;height:20px;} .mig-bar .mig-kbd-btn .mig-kbd-label{color:#cfc8ff;font-weight:600;} .mig-empty{padding:60px 0;text-align:center;color:#9a9aa2;column-span:all;} ${captureCss()}`; } function captureCss() { return ` .mig-capture{position:fixed;inset:0;z-index:${Z + 5};background:rgba(8,8,10,.6); display:flex;align-items:center;justify-content:center; font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;} .mig-capture-box{background:#16161a;color:#e8e8ea;border:1px solid #34343c;border-radius:14px; padding:24px 28px;text-align:center;box-shadow:0 12px 60px rgba(0,0,0,.6);min-width:300px;} .mig-capture-title{font-weight:600;font-size:16px;margin-bottom:8px;} .mig-capture-hint{color:#9a9aa2;font-size:13px;margin-bottom:16px;} .mig-capture-preview{font-size:15px;color:#cfc8ff;background:#26262c;border-radius:8px; padding:10px 14px;min-height:20px;} .mig-capture-reset{margin-top:14px;background:none;border:none;color:#9a9aa2;font-size:12px; cursor:pointer;text-decoration:underline;padding:4px;} .mig-capture-reset:hover{color:#cfc8ff;} `; } function build() { const orphan = document.getElementById("mig-capture-style"); if (orphan) orphan.remove(); const style = document.createElement("style"); style.id = "mig-style"; style.textContent = css(); (document.head || document.documentElement).appendChild(style); root = document.createElement("div"); root.className = "mig-root"; const colW = store.get("colWidth", DEFAULT_COL_WIDTH); const dlControls = ` 0 selected `; const lbDl = ``; root.innerHTML = `
Image Gallery ${dlControls}
× ${lbDl}
‹
›
`; document.documentElement.appendChild(root); grid = root.querySelector(".mig-grid"); scroller = root.querySelector(".mig-scroll"); lb = root.querySelector(".mig-lb"); lbImg = root.querySelector(".mig-lb-img"); lbCounter = root.querySelector(".mig-lb-counter"); root.querySelector(".mig-close-btn").addEventListener("click", close); root.querySelector(".mig-kbd-btn").addEventListener("click", beginCaptureShortcut); refreshShortcutLabel(); root.querySelector(".mig-lb-close").addEventListener("click", (e) => { e.stopPropagation(); closeLightbox(); }); root.querySelector(".mig-lb-prev").addEventListener("click", (e) => { e.stopPropagation(); step(-1); }); root.querySelector(".mig-lb-next").addEventListener("click", (e) => { e.stopPropagation(); step(1); }); lb.addEventListener("click", (e) => { if (e.target !== lbImg) closeLightbox(); }); const range = root.querySelector(".mig-colrange"); range.addEventListener("input", () => { const v = parseInt(range.value, 10); store.set("colWidth", v); grid.style.columnWidth = v + "px"; }); { root.querySelector(".mig-selall-cb").addEventListener("change", (e) => { toggleSelectAll(e.target.checked); }); root.querySelector(".mig-dl-sel").addEventListener("click", () => batchDownload(false)); root.querySelector(".mig-dl-zip").addEventListener("click", () => batchDownload(true)); root.querySelector(".mig-lb-dl").addEventListener("click", (e) => { e.stopPropagation(); if (lightboxIndex >= 0) downloadOne(images[lightboxIndex].url, lightboxIndex); }); } } function selectedEntries() { const out = []; images.forEach((im, i) => { if (selected.has(im.url)) out.push({ url: im.url, index: i }); }); return out; } function selectedUrls() { return selectedEntries().map((e) => e.url); } function setStatus(msg) { const el = root && root.querySelector(".mig-status"); if (el) el.textContent = msg || ""; } function updateSelUI() { if (!root) return; const n = selectedUrls().length; root.querySelector(".mig-selcount").textContent = n + " selected"; const selBtn = root.querySelector(".mig-dl-sel"); const zipBtn = root.querySelector(".mig-dl-zip"); selBtn.disabled = busy || n === 0; zipBtn.disabled = busy || n === 0; selBtn.querySelector("span").textContent = n ? `Download (${n})` : "Download"; zipBtn.querySelector("span").textContent = n ? `ZIP (${n})` : "ZIP"; const all = root.querySelector(".mig-selall-cb"); all.checked = images.length > 0 && n === images.length; all.indeterminate = n > 0 && n < images.length; } function setSelected(url, on, tile) { if (on) selected.add(url); else selected.delete(url); if (tile) { tile.classList.toggle("mig-sel", on); const cb = tile.querySelector(".mig-check"); if (cb) cb.checked = on; } updateSelUI(); } function toggleSelectAll(on) { images.forEach((im) => on ? selected.add(im.url) : selected.delete(im.url)); grid.querySelectorAll(".mig-tile").forEach((tile) => { tile.classList.toggle("mig-sel", on); const cb = tile.querySelector(".mig-check"); if (cb) cb.checked = on; }); updateSelUI(); } async function batchDownload(asZip) { if (busy) return; const entries = selectedEntries(); if (!entries.length) return; if (!asZip) { entries.forEach(({ url, index }, k) => { setTimeout(() => downloadOne(url, index), k * 200); }); const word = entries.length > 1 ? "images" : "image"; setStatus(`Saving ${entries.length} ${word}…`); setTimeout(() => setStatus(""), Math.max(4e3, entries.length * 200 + 1500)); return; } busy = true; updateSelUI(); setStatus("Fetching 0/" + entries.length + "…"); try { const { saved, failed } = await downloadZip(entries, (done, total) => { setStatus("Fetching " + done + "/" + total + "…"); }); if (!saved) setStatus("Could not fetch any image"); else if (failed) setStatus(`Done — saved ${saved}, ${failed} failed`); else setStatus(`Done — saved ${saved} to ${ZIP_FILENAME}.zip`); } catch (e) { setStatus("Download failed"); } finally { busy = false; updateSelUI(); setTimeout(() => setStatus(""), 4e3); } } function render() { images = collect(); grid.innerHTML = ""; if (scroller) scroller.scrollTop = 0; root.querySelector(".mig-count").textContent = images.length + " images"; if (!images.length) { const e = document.createElement("div"); e.className = "mig-empty"; e.textContent = "No images found on this page."; grid.appendChild(e); return; } images.forEach((im, i) => { const tile = document.createElement("a"); tile.className = "mig-tile"; tile.href = im.url; tile.addEventListener("click", (e) => { e.preventDefault(); openLightbox(i); }); const img = document.createElement("img"); img.loading = "lazy"; img.src = im.url; img.addEventListener("error", () => { selected.delete(im.url); tile.remove(); updateSelUI(); }); tile.appendChild(img); { if (selected.has(im.url)) tile.classList.add("mig-sel"); const cb = document.createElement("input"); cb.type = "checkbox"; cb.className = "mig-tile-ctl mig-check"; cb.checked = selected.has(im.url); cb.title = "Select"; cb.addEventListener("click", (e) => e.stopPropagation()); cb.addEventListener("change", (e) => setSelected(im.url, e.target.checked, tile)); tile.appendChild(cb); const dl = document.createElement("button"); dl.className = "mig-tile-ctl mig-dl"; dl.title = "Download"; dl.innerHTML = DL_ICON; dl.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); downloadOne(im.url, i); }); tile.appendChild(dl); } grid.appendChild(tile); }); updateSelUI(); } function openLightbox(i) { lightboxIndex = i; lbImg.src = images[i].url; lbImg.alt = "Image " + (i + 1); lbCounter.textContent = i + 1 + " / " + images.length; lb.classList.add("open"); } function closeLightbox() { lb.classList.remove("open"); lightboxIndex = -1; } function step(dir) { if (lightboxIndex < 0 || !images.length) return; lightboxIndex = (lightboxIndex + dir + images.length) % images.length; openLightbox(lightboxIndex); } function openGallery() { if (open) return; if (!root) build(); root.style.display = "flex"; open = true; render(); } function close() { if (!open) return; closeLightbox(); root.style.display = "none"; open = false; } function toggle() { open ? close() : openGallery(); } function onKeyDown(e) { if (capturing) return; if (matchesShortcut(e, currentShortcut())) { e.preventDefault(); toggle(); return; } if (!open) return; if (lightboxIndex >= 0) { if (e.key === "Escape") { e.preventDefault(); closeLightbox(); } else if (e.key === "ArrowRight") { e.preventDefault(); step(1); } else if (e.key === "ArrowLeft") { e.preventDefault(); step(-1); } } else if (e.key === "Escape") { e.preventDefault(); close(); } } function createLauncher() { const s = document.createElement("style"); s.textContent = ` .mig-launch{position:fixed;right:18px;bottom:18px;z-index:${Z - 1}; width:48px;height:48px;border-radius:50%;border:none;cursor:pointer; background:#7c6cff;color:#fff;box-shadow:0 4px 16px rgba(0,0,0,.35); display:flex;align-items:center;justify-content:center; transition:transform .12s ease,background .12s ease;opacity:.9;} .mig-launch:hover{transform:scale(1.08);background:#6a5af0;opacity:1;} .mig-launch svg{width:22px;height:22px;} `; (document.head || document.documentElement).appendChild(s); const btn = document.createElement("button"); btn.className = "mig-launch"; btn.innerHTML = ''; btn.addEventListener("click", toggle); launchBtn = btn; refreshShortcutLabel(); document.documentElement.appendChild(btn); } function refreshShortcutLabel() { const label = shortcutLabel(currentShortcut(), isMac()); if (launchBtn) launchBtn.title = "Open Image Gallery (" + label + ")"; const kbd = root && root.querySelector(".mig-kbd-btn"); if (kbd) { kbd.title = "Click to change the shortcut (currently " + label + ")"; const kbdLabel = kbd.querySelector(".mig-kbd-label"); if (kbdLabel) kbdLabel.textContent = label; } } function modifierHint() { return isMac() ? "Control / Option / ⌘" : "Ctrl / Alt"; } function resetShortcut() { store.set("shortcut", null); refreshShortcutLabel(); } function beginCaptureShortcut() { if (document.querySelector(".mig-capture")) return; ensureStyle(); const defaultLabel = shortcutLabel(SHORTCUT, isMac()); const overlay = document.createElement("div"); overlay.className = "mig-capture"; overlay.innerHTML = `
Press the new gallery shortcut
Hold ${modifierHint()} and a key. Esc to cancel.
…
`; const preview = overlay.querySelector(".mig-capture-preview"); function cleanup() { capturing = false; document.removeEventListener("keydown", onCap, true); overlay.remove(); } function confirmAndClose(message) { preview.textContent = message; document.removeEventListener("keydown", onCap, true); setTimeout(cleanup, 900); } function onCap(e) { e.preventDefault(); e.stopPropagation(); if (e.code === "Escape" && !e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) { cleanup(); return; } if (isModifierKey(e.code)) { preview.textContent = shortcutLabel(shortcutFromEvent(e), isMac()) + "…"; return; } const sc = shortcutFromEvent(e); if (!isValidShortcut(sc)) { preview.textContent = "Add " + modifierHint() + " — a bare key would clash with normal page use"; return; } store.set("shortcut", sc); refreshShortcutLabel(); confirmAndClose("Saved: " + shortcutLabel(sc, isMac())); } overlay.querySelector(".mig-capture-reset").addEventListener("click", () => { resetShortcut(); confirmAndClose("Reset to default: " + defaultLabel); }); overlay.addEventListener("click", (e) => { if (e.target === overlay) cleanup(); }); capturing = true; document.addEventListener("keydown", onCap, true); document.documentElement.appendChild(overlay); } function ensureStyle() { if (document.getElementById("mig-style") || document.getElementById("mig-capture-style")) return; const s = document.createElement("style"); s.id = "mig-capture-style"; s.textContent = captureCss(); (document.head || document.documentElement).appendChild(s); } function init() { document.addEventListener("keydown", onKeyDown, true); createLauncher(); if (typeof GM_registerMenuCommand === "function") { GM_registerMenuCommand("Open Image Gallery (" + shortcutLabel(currentShortcut(), isMac()) + ")", toggle); GM_registerMenuCommand("Set gallery shortcut…", beginCaptureShortcut); GM_registerMenuCommand("Reset gallery shortcut to default", resetShortcut); } } init(); })();