// ==UserScript== // @name 珠宝AI手机端适配 0.2.6 // @namespace https://zbai.art/ // @version 0.2.6 // @description iPhone/Stay mobile adapter: preview, compose panel, upload, history, download. // @match http://zbai.art/* // @match https://zbai.art/* // @match https://jew.haistudio.ai/* // @grant GM_addStyle // @run-at document-start // ==/UserScript== (function () { "use strict"; const ROOT_ID = "zbai-mobile-root-v026"; const STYLE_ID = "zbai-mobile-style-v026"; const CLS = { enabled: "zbai-m-enabled", lock: "zbai-m-lock", compose: "zbai-m-compose", composeOpen: "zbai-m-compose-open", history: "zbai-m-history", historyOpen: "zbai-m-history-open", ancestorFix: "zbai-m-ancestor-fix", }; let activeSurface = ""; let toastTimer = 0; let refreshTimer = 0; fixViewport(); addStyles(); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot, { once: true }); } else { boot(); } function boot() { if (!document.body || document.getElementById(ROOT_ID)) return; document.documentElement.classList.add(CLS.enabled); document.body.classList.add(CLS.enabled); const root = buildRoot(); document.body.appendChild(root); root.addEventListener("click", onRootClick); refreshTargets(); window.addEventListener("resize", scheduleRefresh); window.addEventListener("orientationchange", () => { closeSurface(); setTimeout(refreshTargets, 350); }); document.addEventListener("click", () => { setTimeout(promoteFloatLayers, 80); }, true); const observer = new MutationObserver(scheduleRefresh); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "src"] }); setTimeout(refreshTargets, 600); setTimeout(refreshTargets, 1500); setTimeout(refreshTargets, 3000); } function buildRoot() { const root = document.createElement("div"); root.id = ROOT_ID; root.innerHTML = `
`; return root; } function onRootClick(event) { const button = event.target.closest("[data-action]"); if (!button) return; const action = button.dataset.action; if (action === "close") closeSurface(); if (action === "compose") openCompose(); if (action === "upload") openUpload(); if (action === "history") openHistory(); if (action === "download") openDownload(); } function openCompose() { const panel = findComposePanel(); if (!panel) { toast("没找到右侧制作页:先点左上角 + 或刷新页面后再点生成"); return; } if (activeSurface === "compose" && panel.classList.contains(CLS.composeOpen)) { closeSurface(); return; } openSurface("compose", panel); focusPrompt(panel); toast("已打开制作页"); } async function openUpload() { const panel = findComposePanel(); if (!panel) { toast("没找到上传区:先点生成打开制作页"); return; } openSurface("compose", panel); await delay(120); let upload = findUploadControl(panel); if (!upload) { const uploadTab = findByText(panel, /图生|上传|参考图|产品图/); uploadTab?.click(); await delay(300); upload = findUploadControl(panel); } if (!upload) { toast("已打开制作页,请点里面的上传图片"); return; } upload.scrollIntoView({ block: "center", behavior: "smooth" }); if (upload.tagName === "INPUT" && upload.type === "file") { upload.click(); return; } upload.click(); } function openHistory() { const rail = findHistoryRail(); if (!rail) { toast("没找到左侧历史栏"); return; } if (activeSurface === "history" && rail.classList.contains(CLS.historyOpen)) { closeSurface(); return; } openSurface("history", rail); toast("已打开历史栏"); } async function openDownload() { closeSurface(); await delay(120); const download = findDownloadControl(); if (download) { download.click(); return; } const result = findMainPreviewImage(); if (!result) { toast("先生成或点开一张图片,再点下载"); return; } result.click(); await delay(350); const download2 = findDownloadControl(); if (download2) { download2.click(); return; } toast("预览已打开,请点预览里的下载图标"); } function openSurface(type, node) { closeSurface(); activeSurface = type; markAncestors(node); if (type === "compose") { node.classList.add(CLS.compose, CLS.composeOpen); node.scrollTop = 0; } if (type === "history") { node.classList.add(CLS.history, CLS.historyOpen); } document.documentElement.classList.add(CLS.lock); document.body.classList.add(CLS.lock); const root = document.getElementById(ROOT_ID); root?.setAttribute("data-open", type); setTimeout(promoteFloatLayers, 60); } function closeSurface() { activeSurface = ""; document.querySelectorAll("." + CLS.composeOpen).forEach((node) => { node.classList.remove(CLS.composeOpen); }); document.querySelectorAll("." + CLS.historyOpen).forEach((node) => { node.classList.remove(CLS.historyOpen); }); document.querySelectorAll("." + CLS.ancestorFix).forEach((node) => { node.classList.remove(CLS.ancestorFix); }); document.documentElement.classList.remove(CLS.lock); document.body.classList.remove(CLS.lock); const root = document.getElementById(ROOT_ID); root?.removeAttribute("data-open"); } function refreshTargets() { fixViewport(); document.documentElement.classList.add(CLS.enabled); document.body?.classList.add(CLS.enabled); const compose = findComposePanel(); if (compose) compose.classList.add(CLS.compose); const history = findHistoryRail(); if (history) history.classList.add(CLS.history); promoteFloatLayers(); } function scheduleRefresh() { if (refreshTimer) return; refreshTimer = window.setTimeout(() => { refreshTimer = 0; refreshTargets(); }, 220); } function findComposePanel() { const candidates = []; const anchors = [ ...document.querySelectorAll("textarea"), ...document.querySelectorAll("input[type='file']"), ...document.querySelectorAll("button, label, div, section, aside, main, form, [role='button']") ]; for (const node of anchors) { const text = getText(node); const tag = node.tagName; if ( tag === "TEXTAREA" || tag === "INPUT" || /在此输入|Specify image generation language|上传|参考图|产品图|快捷提示词|选个风格|生成比例|选择模型|选择模板|立即创作|开始创作|生成图片|生成/.test(text) ) { const panel = walkParents(node, isLikelyComposePanel); if (panel && !candidates.includes(panel)) candidates.push(panel); } } if (!candidates.length) { const blocks = [...document.querySelectorAll("aside, section, main, form, div")] .filter(isLikelyComposePanel); candidates.push(...blocks); } if (!candidates.length) return null; candidates.sort((a, b) => scoreComposePanel(b) - scoreComposePanel(a)); return candidates[0]; } function isLikelyComposePanel(node) { if (!node || node.closest("#" + ROOT_ID)) return false; const rect = node.getBoundingClientRect(); const style = getComputedStyle(node); if (style.display === "none" || style.visibility === "hidden") return false; if (rect.width < 220 || rect.height < 240) return false; const text = getText(node); if (!text) return false; if (text.length > 8000) return false; const hasPrompt = text.includes("在此输入") || text.includes("Specify image generation language") || node.querySelector("textarea"); const hasUpload = text.includes("上传") || text.includes("参考图") || text.includes("产品图") || node.querySelector("input[type='file']"); const hasStyle = text.includes("选个风格") || text.includes("快捷提示词") || text.includes("选择模板") || text.includes("选择模型"); const hasRatio = text.includes("生成比例") || text.includes("1:1") || text.includes("3:4") || text.includes("4:3") || text.includes("9:16"); const hasCreate = text.includes("立即创作") || text.includes("开始创作") || text.includes("生成"); const imageCount = node.querySelectorAll("img").length; let hit = 0; if (hasPrompt) hit += 3; if (hasUpload) hit += 2; if (hasStyle) hit += 2; if (hasRatio) hit += 2; if (hasCreate) hit += 1; if (imageCount > 20 && !hasPrompt) hit -= 3; return hit >= 4; } function scoreComposePanel(node) { const text = getText(node); const rect = node.getBoundingClientRect(); let score = 0; if (node.querySelector("textarea")) score += 30; if (node.querySelector("input[type='file']")) score += 20; if (/在此输入|Specify image generation language/.test(text)) score += 35; if (/上传|参考图|产品图/.test(text)) score += 20; if (/选个风格|快捷提示词|选择模板|选择模型/.test(text)) score += 18; if (/生成比例|1:1|3:4|4:3|9:16/.test(text)) score += 15; if (/立即创作|开始创作|生成/.test(text)) score += 10; if (rect.left > window.innerWidth * 0.45) score += 20; if (rect.width >= 260 && rect.width <= 620) score += 8; if (text.length > 5000) score -= 15; return score; } function findHistoryRail() { const images = [...document.images].filter((img) => { const rect = img.getBoundingClientRect(); return rect.width >= 28 && rect.width <= 120 && rect.height >= 28 && rect.height <= 140 && rect.left < 140; }); const candidates = []; for (const img of images) { const rail = walkParents(img, (node) => { if (!node || node.closest("#" + ROOT_ID)) return false; const rect = node.getBoundingClientRect(); const count = node.querySelectorAll("img").length; return count >= 4 && rect.left < 130 && rect.width >= 46 && rect.width <= 180 && rect.height >= 300; }); if (rail && !candidates.includes(rail)) candidates.push(rail); } if (!candidates.length) return null; candidates.sort((a, b) => { const ar = a.getBoundingClientRect(); const br = b.getBoundingClientRect(); return (b.querySelectorAll("img").length * br.height) - (a.querySelectorAll("img").length * ar.height); }); return candidates[0]; } function findUploadControl(scope) { if (!scope) return null; const file = scope.querySelector("input[type='file']"); if (file) return file; return [...scope.querySelectorAll("button, label, div, span, [role='button']")] .find((node) => { const text = getText(node); return /上传|上传图片|上传参考图|产品图|参考图/.test(text) && isClickableEnough(node); }); } function findDownloadControl() { const selectors = [ "[download]", "[title*='下载']", "[aria-label*='下载']", "a", "button", "[role='button']", "i", "span", "div" ]; const nodes = [...document.querySelectorAll(selectors.join(","))]; return nodes.find((node) => { if (!isClickableEnough(node)) return false; const text = getText(node); const sign = [ text, node.getAttribute("title") || "", node.getAttribute("aria-label") || "", node.getAttribute("class") || "", node.innerHTML || "" ].join(" "); if (/上传/.test(sign)) return false; return /下载|download|xiazai|icon-download/i.test(sign); }); } function findMainPreviewImage() { const images = [...document.images] .map((img) => ({ img, rect: img.getBoundingClientRect() })) .filter(({ img, rect }) => { const src = img.currentSrc || img.src || ""; return rect.width > 180 && rect.height > 180 && !/logo|avatar|icon/i.test(src); }) .sort((a, b) => { return (b.rect.width * b.rect.height) - (a.rect.width * a.rect.height); }); return images[0]?.img || null; } function focusPrompt(panel) { const input = panel.querySelector("textarea"); if (!input) return; setTimeout(() => { input.scrollIntoView({ block: "center", behavior: "smooth" }); try { input.focus({ preventScroll: true }); } catch (e) { input.focus(); } }, 180); } function findByText(scope, pattern) { return [...scope.querySelectorAll("button, label, div, span, [role='button']")] .find((node) => { const text = getText(node); return pattern.test(text) && isClickableEnough(node); }); } function isClickableEnough(node) { if (!node || node.closest("#" + ROOT_ID)) return false; const rect = node.getBoundingClientRect(); const style = getComputedStyle(node); return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden"; } function walkParents(start, predicate) { let node = start; for (let i = 0; node && i < 18; i += 1) { if (predicate(node)) return node; node = node.parentElement; } return null; } function getText(node) { return (node?.innerText || node?.textContent || "") .replace(/\s+/g, " ") .trim(); } function markAncestors(node) { let parent = node.parentElement; for (let i = 0; parent && parent !== document.body && i < 12; i += 1) { parent.classList.add(CLS.ancestorFix); parent = parent.parentElement; } } function promoteFloatLayers() { const nodes = document.querySelectorAll([ ".ant-select-dropdown", ".ant-dropdown", ".el-select-dropdown", ".arco-select-popup", "[role='listbox']", "[data-radix-popper-content-wrapper]" ].join(",")); nodes.forEach((node) => { node.style.setProperty("z-index", "2147483646", "important"); }); } function toast(message) { const box = document.querySelector("#" + ROOT_ID + " .zbai-m-toast"); if (!box) return; box.textContent = message; box.classList.add("show"); clearTimeout(toastTimer); toastTimer = setTimeout(() => { box.classList.remove("show"); }, 2600); } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function fixViewport() { let meta = document.querySelector("meta[name='viewport']"); if (!meta) { meta = document.createElement("meta"); meta.name = "viewport"; if (document.head) { document.head.appendChild(meta); } else { document.documentElement.appendChild(meta); } } meta.setAttribute( "content", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" ); } function addStyles() { if (document.getElementById(STYLE_ID)) return; const css = ` html.${CLS.enabled}, body.${CLS.enabled} { width: 100vw !important; max-width: 100vw !important; min-width: 0 !important; overflow-x: hidden !important; -webkit-text-size-adjust: 100% !important; zoom: 1 !important; background: #f6f6fa !important; } body.${CLS.lock} { overflow: hidden !important; touch-action: none !important; } body.${CLS.enabled} #${ROOT_ID} { position: fixed !important; inset: 0 !important; z-index: 2147483640 !important; pointer-events: none !important; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif !important; } body.${CLS.enabled} #${ROOT_ID} .zbai-m-dock { position: fixed !important; left: max(10px, env(safe-area-inset-left)) !important; right: max(10px, env(safe-area-inset-right)) !important; bottom: calc(10px + env(safe-area-inset-bottom)) !important; z-index: 2147483645 !important; display: grid !important; grid-template-columns: repeat(4, minmax(0, 1fr)) !important; gap: 8px !important; padding: 8px !important; box-sizing: border-box !important; border: 1px solid rgba(20, 22, 28, .12) !important; border-radius: 10px !important; background: rgba(255, 255, 255, .96) !important; box-shadow: 0 10px 28px rgba(20, 24, 35, .18) !important; backdrop-filter: blur(14px) !important; pointer-events: auto !important; } body.${CLS.enabled} #${ROOT_ID} .zbai-m-dock button { height: 48px !important; min-height: 48px !important; padding: 0 !important; border: 0 !important; border-radius: 7px !important; background: #f2f3f7 !important; color: #17191f !important; font-size: 16px !important; font-weight: 700 !important; line-height: 48px !important; text-align: center !important; pointer-events: auto !important; touch-action: manipulation !important; } body.${CLS.enabled} #${ROOT_ID} .zbai-m-dock button:first-child { background: #111318 !important; color: #fff !important; } body.${CLS.enabled} #${ROOT_ID} .zbai-m-backdrop { position: fixed !important; inset: 0 !important; z-index: 2147483641 !important; border: 0 !important; background: rgba(8, 10, 16, .35) !important; opacity: 0 !important; pointer-events: none !important; } body.${CLS.enabled} #${ROOT_ID}[data-open] .zbai-m-backdrop { opacity: 1 !important; pointer-events: auto !important; } body.${CLS.enabled} #${ROOT_ID} .zbai-m-close { display: none !important; position: fixed !important; top: max(10px, env(safe-area-inset-top)) !important; left: max(10px, env(safe-area-inset-left)) !important; z-index: 2147483647 !important; height: 42px !important; min-width: 66px !important; padding: 0 14px !important; border: 1px solid rgba(20, 22, 28, .12) !important; border-radius: 8px !important; background: rgba(255, 255, 255, .96) !important; color: #17191f !important; box-shadow: 0 8px 22px rgba(20, 24, 35, .16) !important; font-size: 15px !important; font-weight: 700 !important; pointer-events: auto !important; } body.${CLS.enabled} #${ROOT_ID}[data-open] .zbai-m-close { display: block !important; } body.${CLS.enabled} #${ROOT_ID} .zbai-m-toast { position: fixed !important; left: 18px !important; right: 18px !important; bottom: calc(82px + env(safe-area-inset-bottom)) !important; z-index: 2147483647 !important; padding: 12px 14px !important; border-radius: 9px !important; background: rgba(17, 19, 24, .94) !important; color: #fff !important; font-size: 14px !important; line-height: 1.45 !important; opacity: 0 !important; transform: translateY(8px) !important; transition: opacity .16s ease, transform .16s ease !important; pointer-events: none !important; } body.${CLS.enabled} #${ROOT_ID} .zbai-m-toast.show { opacity: 1 !important; transform: translateY(0) !important; } body.${CLS.enabled} .${CLS.ancestorFix} { transform: none !important; perspective: none !important; filter: none !important; contain: none !important; overflow: visible !important; } body.${CLS.enabled} .${CLS.compose}.${CLS.composeOpen} { position: fixed !important; z-index: 2147483643 !important; top: 0 !important; right: 0 !important; bottom: calc(74px + env(safe-area-inset-bottom)) !important; left: 0 !important; width: 100vw !important; min-width: 0 !important; max-width: 100vw !important; height: auto !important; max-height: none !important; margin: 0 !important; padding: calc(58px + env(safe-area-inset-top)) 12px 108px 12px !important; box-sizing: border-box !important; overflow-y: auto !important; overflow-x: hidden !important; overscroll-behavior: contain !important; background: #fff !important; border-radius: 0 !important; box-shadow: none !important; transform: none !important; opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; } body.${CLS.enabled} .${CLS.compose}.${CLS.composeOpen} * { max-width: 100% !important; box-sizing: border-box !important; } body.${CLS.enabled} .${CLS.compose}.${CLS.composeOpen} textarea, body.${CLS.enabled} .${CLS.compose}.${CLS.composeOpen} input { max-width: 100% !important; font-size: 16px !important; } body.${CLS.enabled} .${CLS.compose}.${CLS.composeOpen} textarea { min-height: 120px !important; } body.${CLS.enabled} .${CLS.compose}.${CLS.composeOpen} button, body.${CLS.enabled} .${CLS.compose}.${CLS.composeOpen} [role="button"], body.${CLS.enabled} .${CLS.compose}.${CLS.composeOpen} label { min-height: 40px !important; touch-action: manipulation !important; } body.${CLS.enabled} .${CLS.compose}.${CLS.composeOpen} img { max-width: 100% !important; height: auto !important; object-fit: contain !important; } body.${CLS.enabled} .${CLS.history}.${CLS.historyOpen} { position: fixed !important; z-index: 2147483643 !important; top: 0 !important; bottom: calc(74px + env(safe-area-inset-bottom)) !important; left: 0 !important; width: min(28vw, 128px) !important; min-width: 92px !important; max-width: 132px !important; height: auto !important; margin: 0 !important; padding: calc(58px + env(safe-area-inset-top)) 8px 96px 8px !important; box-sizing: border-box !important; overflow-y: auto !important; overflow-x: hidden !important; background: #f7f7fb !important; border-radius: 0 10px 10px 0 !important; box-shadow: 10px 0 30px rgba(20, 24, 35, .22) !important; transform: none !important; opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; } body.${CLS.enabled} .${CLS.history}.${CLS.historyOpen} img { width: 100% !important; max-width: 100% !important; height: auto !important; } body.${CLS.enabled} .ant-select-dropdown, body.${CLS.enabled} .ant-dropdown, body.${CLS.enabled} .el-select-dropdown, body.${CLS.enabled} .arco-select-popup, body.${CLS.enabled} [role="listbox"], body.${CLS.enabled} [data-radix-popper-content-wrapper] { z-index: 2147483646 !important; } `; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = css; if (typeof GM_addStyle === "function") { GM_addStyle(css); } else if (document.head) { document.head.appendChild(style); } else { document.documentElement.appendChild(style); } } })();