// ==UserScript== // @name 珠宝AI网页适配 // @namespace https://zbai.art/ // @version 0.2.5 // @description Make Jew AI image creation controls usable on iPhone/Stay without breaking page init. // @match http://zbai.art/* // @match https://zbai.art/* // @match https://jew.haistudio.ai/* // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function () { "use strict"; const MOBILE_QUERY = "(max-width: 820px)"; const ROOT_ID = "zbai-mobile-root-v025"; const CLASS = { enabled: "zbai-mobile-enabled-v025", compose: "zbai-mobile-compose-source-v025", composeOpen: "zbai-mobile-compose-open-v025", history: "zbai-mobile-history-source-v025", historyOpen: "zbai-mobile-history-open-v025", lock: "zbai-mobile-lock-v025", }; let activeSurface = ""; let toastTimer = 0; addStyles(); waitForBody(boot); function boot() { if (document.getElementById(ROOT_ID)) return; const root = buildRoot(); document.body.appendChild(root); syncMode(); refreshTargets(); root.addEventListener("click", onRootClick); window.addEventListener("resize", throttle(() => { syncMode(); refreshTargets(); }, 200)); const observer = new MutationObserver(throttle(refreshTargets, 300)); observer.observe(document.body, { childList: true, subtree: true, }); } function buildRoot() { const root = document.createElement("div"); root.id = ROOT_ID; root.innerHTML = `
`; return root; } function onRootClick(event) { const button = event.target.closest("[data-zbai-action]"); if (!button || !isMobile()) return; const action = button.dataset.zbaiAction; if (action === "close") closeSurface(); if (action === "compose") toggleCompose(); if (action === "upload") openUpload(); if (action === "history") toggleHistory(); if (action === "download") openDownload(); if (action === "auth") toggleAuth(); } function toggleCompose() { const panel = findComposePanel(); if (!panel) { toast("还没找到生成面板,等页面加载完再点一次"); return; } if (activeSurface === "compose") { closeSurface(); return; } openSurface("compose", panel); const prompt = panel.querySelector("textarea"); if (prompt) { setTimeout(() => { prompt.scrollIntoView({ block: "center" }); }, 120); } } async function openUpload() { const panel = findComposePanel(); if (!panel) { toast("还没找到上传区,先进入图片创作页"); return; } openSurface("compose", panel); await delay(150); const upload = findClickableByText(panel, "图片上传") || findClickableByText(panel, "上传") || panel.querySelector('input[type="file"]') || panel.querySelector('[class*="upload" i]'); if (!upload) { toast("已打开生成面板,请在里面点图片上传"); return; } upload.scrollIntoView({ block: "center" }); upload.click(); } function toggleHistory() { const rail = findHistoryRail(); if (!rail) { toast("还没找到历史记录"); return; } if (activeSurface === "history") { closeSurface(); return; } openSurface("history", rail); } async function openDownload() { closeSurface(); const download = findDownloadControl(); if (download) { download.click(); return; } const result = findPrimaryResult(); if (!result) { toast("先生成或打开一张图片,再点下载"); return; } result.click(); await delay(300); const previewDownload = findDownloadControl(); if (previewDownload) { previewDownload.click(); return; } toast("预览已打开,请点预览里的下载按钮"); } function openSurface(surface, node) { closeSurface(); activeSurface = surface; if (surface === "compose") { node.classList.add(CLASS.composeOpen); node.scrollTop = 0; } if (surface === "history") { node.classList.add(CLASS.historyOpen); } document.body.classList.add(CLASS.lock); const root = document.getElementById(ROOT_ID); if (root) root.setAttribute("data-zbai-surface", surface); } function closeSurface() { activeSurface = ""; document.querySelectorAll("." + CLASS.composeOpen).forEach((node) => { node.classList.remove(CLASS.composeOpen); }); document.querySelectorAll("." + CLASS.historyOpen).forEach((node) => { node.classList.remove(CLASS.historyOpen); }); document.body.classList.remove(CLASS.lock); const root = document.getElementById(ROOT_ID); if (root) root.removeAttribute("data-zbai-surface"); } function refreshTargets() { if (!isMobile()) return; document.body.classList.add(CLASS.enabled); const compose = findComposePanel(); if (compose) { compose.classList.add(CLASS.compose); } const history = findHistoryRail(); if (history) { history.classList.add(CLASS.history); } refreshAuthControl(); } function syncMode() { const enabled = isMobile(); if (!document.body) return; document.body.classList.toggle(CLASS.enabled, enabled); if (!enabled) { closeSurface(); } } function refreshAuthControl() { const button = document.querySelector(`#${ROOT_ID} .zbai-mobile-auth`); if (!button) return; const loggedIn = Boolean(findProfileAvatar()); button.textContent = loggedIn ? "退出" : "登录"; } async function toggleAuth() { if (findProfileAvatar()) { const avatar = findProfileAvatar(); avatar.click(); await delay(250); const logout = findAuthTextControl(/退出登录|退出账号|退出/) || findAuthTextControl(/登出/); if (logout) { logout.click(); } else { toast("账号菜单已打开,请点退出"); } return; } const login = findAuthTextControl(/^(登录|登录\/注册|注册\/登录)$/) || findAuthTextControl(/登录/); if (login) { login.click(); } else { toast("还没找到登录入口,请刷新后再点"); } } function findComposePanel() { const anchors = [ ...document.querySelectorAll("textarea"), ...document.querySelectorAll('input[type="file"]'), ...Array.from(document.querySelectorAll("button, label, [role='button'], div, span")) .filter((node) => /上传|生成比例|选个风格|快捷提示词|在此输入/.test(normalizedText(node))), ]; for (const anchor of anchors) { const panel = walkParents(anchor, isLikelyComposePanel); if (panel) return panel; } return null; } function isLikelyComposePanel(node) { if (!node || !node.querySelector) return false; const text = normalizedText(node); const hasInput = Boolean(node.querySelector("textarea")) || text.includes("在此输入") || text.includes("Specify image generation language"); const hasStyle = text.includes("选个风格") || text.includes("快捷提示词") || text.includes("生成比例"); const hasUpload = text.includes("上传") || Boolean(node.querySelector('input[type="file"]')); const rect = node.getBoundingClientRect(); return hasInput && hasStyle && hasUpload && rect.width > 220 && rect.height > 300; } function findHistoryRail() { const images = Array.from(document.images).filter((image) => { const rect = image.getBoundingClientRect(); return rect.width > 24 && rect.width < 180 && rect.height > 24; }); for (const image of images) { const rail = walkParents(image, (node) => { if (!node || !node.querySelectorAll) return false; const rect = node.getBoundingClientRect(); const count = node.querySelectorAll("img").length; return count >= 2 && rect.height > 200 && rect.width > 40 && rect.width < 260; }); if (rail) return rail; } return null; } function findDownloadControl() { const selectors = [ '[title*="下载"]', '[aria-label*="下载"]', '[download]', '[class*="download" i]', ]; const bySelector = selectors.flatMap((selector) => { return Array.from(document.querySelectorAll(selector)); }); const byText = Array.from(document.querySelectorAll("button, a, [role='button'], div, span")) .filter((node) => normalizedText(node).includes("下载")); return [...bySelector, ...byText].find(isUsable); } function findPrimaryResult() { return Array.from(document.images) .map((image) => { return { image, rect: image.getBoundingClientRect(), }; }) .filter(({ image, rect }) => { const src = image.currentSrc || image.src || ""; return ( isUsable(image) && rect.width > 140 && rect.height > 140 && !src.includes("logo") && !src.includes("avatar") ); }) .sort((a, b) => { return b.rect.width * b.rect.height - a.rect.width * a.rect.height; }) .map(({ image }) => image)[0]; } function findClickableByText(scope, text) { return Array.from(scope.querySelectorAll("button, a, label, [role='button'], div, span")) .find((node) => { return normalizedText(node).includes(text) && isUsable(node); }); } function findProfileAvatar() { const images = Array.from(document.querySelectorAll( 'img[src*="avatar" i], img[src*="profile" i], img[src*="head" i]' )); return images.find((image) => { if (!isUsable(image)) return false; const rect = image.getBoundingClientRect(); return ( rect.top < 130 && rect.right > window.innerWidth * 0.58 && rect.width >= 22 && rect.width <= 80 && rect.height >= 22 && rect.height <= 80 ); }); } function findAuthTextControl(pattern) { return Array.from(document.querySelectorAll( "button, a, [role='button'], [role='menuitem'], li, div, span" )) .filter((node) => !node.closest(`#${ROOT_ID}`)) .find((node) => { const text = normalizedText(node); return text.length <= 20 && pattern.test(text) && isUsable(node); }); } function walkParents(start, predicate) { let node = start; for (let depth = 0; node && depth < 18; depth += 1) { if (predicate(node)) return node; node = node.parentElement; } return null; } function normalizedText(node) { return (node?.innerText || node?.textContent || "") .replace(/\s+/g, " ") .trim(); } function isUsable(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 isMobile() { const isTouchDevice = navigator.maxTouchPoints > 0; const mobileAgent = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent); const phoneSizedScreen = Math.min(screen.width || 0, screen.height || 0) <= 900; return ( window.matchMedia(MOBILE_QUERY).matches || isTouchDevice || mobileAgent || phoneSizedScreen ); } function toast(message) { const box = document.querySelector(`#${ROOT_ID} .zbai-mobile-toast`); if (!box) return; box.textContent = message; box.classList.add("is-visible"); window.clearTimeout(toastTimer); toastTimer = window.setTimeout(() => { box.classList.remove("is-visible"); }, 2500); } function delay(ms) { return new Promise((resolve) => { window.setTimeout(resolve, ms); }); } function throttle(callback, wait) { let timer = 0; return function () { if (timer) return; timer = window.setTimeout(() => { timer = 0; callback(); }, wait); }; } function waitForBody(callback) { if (document.body) { callback(); return; } window.addEventListener("DOMContentLoaded", callback, { once: true }); } function addStyles() { const css = ` #${ROOT_ID} { display: none; } body.${CLASS.lock} { overflow: hidden !important; } body.${CLASS.enabled} #${ROOT_ID} { display: block; position: fixed; inset: 0; z-index: 2147483600; pointer-events: none; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif; } body.${CLASS.enabled} #${ROOT_ID} .zbai-mobile-auth { position: fixed; top: max(10px, env(safe-area-inset-top)); right: max(10px, env(safe-area-inset-right)); z-index: 2147483603; min-width: 58px; min-height: 38px; padding: 0 12px; border: 0; border-radius: 8px; color: #fff; background: rgba(23, 25, 31, .94); box-shadow: 0 8px 24px rgba(0, 0, 0, .18); font-size: 14px; font-weight: 700; pointer-events: auto; } body.${CLASS.enabled} #${ROOT_ID} .zbai-mobile-close { display: none; position: fixed; top: max(10px, env(safe-area-inset-top)); left: max(10px, env(safe-area-inset-left)); z-index: 2147483606; min-height: 38px; padding: 0 12px; border: 0; border-radius: 8px; color: #17191f; background: rgba(255, 255, 255, .96); box-shadow: 0 8px 24px rgba(0, 0, 0, .18); font-size: 14px; font-weight: 700; pointer-events: auto; } body.${CLASS.enabled} #${ROOT_ID}[data-zbai-surface] .zbai-mobile-close { display: block; } body.${CLASS.enabled} #${ROOT_ID} .zbai-mobile-dock { position: fixed; left: max(10px, env(safe-area-inset-left)); right: max(10px, env(safe-area-inset-right)); bottom: calc(10px + env(safe-area-inset-bottom)); z-index: 2147483602; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; padding: 8px; border-radius: 10px; background: rgba(255, 255, 255, .96); box-shadow: 0 10px 30px rgba(0, 0, 0, .16); pointer-events: auto; } body.${CLASS.enabled} #${ROOT_ID} .zbai-mobile-dock button { min-height: 44px; border: 0; border-radius: 8px; color: #17191f; background: #f1f2f5; font-size: 15px; font-weight: 700; } body.${CLASS.enabled} #${ROOT_ID} .zbai-mobile-dock button:first-child { color: #fff; background: #17191f; } body.${CLASS.enabled} #${ROOT_ID} .zbai-mobile-backdrop { position: fixed; inset: 0; z-index: 2147483600; border: 0; background: rgba(0, 0, 0, .36); opacity: 0; pointer-events: none; } body.${CLASS.enabled} #${ROOT_ID}[data-zbai-surface] .zbai-mobile-backdrop { opacity: 1; pointer-events: auto; } body.${CLASS.enabled} #${ROOT_ID} .zbai-mobile-toast { position: fixed; left: 18px; right: 18px; bottom: calc(78px + env(safe-area-inset-bottom)); z-index: 2147483607; padding: 11px 13px; border-radius: 8px; color: #fff; background: rgba(23, 25, 31, .94); font-size: 13px; line-height: 1.45; opacity: 0; transform: translateY(8px); transition: opacity .16s ease, transform .16s ease; pointer-events: none; } body.${CLASS.enabled} #${ROOT_ID} .zbai-mobile-toast.is-visible { opacity: 1; transform: translateY(0); } body.${CLASS.enabled} .${CLASS.compose}.${CLASS.composeOpen} { position: fixed !important; z-index: 2147483601 !important; top: 0 !important; right: 0 !important; bottom: calc(68px + 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(56px + env(safe-area-inset-top)) 12px 22px 12px !important; box-sizing: border-box !important; overflow-y: auto !important; overflow-x: hidden !important; overscroll-behavior: contain !important; border-radius: 0 !important; background: #fff !important; box-shadow: none !important; transform: none !important; zoom: 1 !important; } body.${CLASS.enabled} .${CLASS.compose}.${CLASS.composeOpen} * { max-width: 100% !important; box-sizing: border-box !important; } body.${CLASS.enabled} .${CLASS.compose}.${CLASS.composeOpen} textarea, body.${CLASS.enabled} .${CLASS.compose}.${CLASS.composeOpen} input { max-width: 100% !important; font-size: 16px !important; } body.${CLASS.enabled} .${CLASS.compose}.${CLASS.composeOpen} textarea { min-height: 120px !important; } body.${CLASS.enabled} .${CLASS.compose}.${CLASS.composeOpen} img { max-width: 100% !important; height: auto !important; object-fit: contain !important; } body.${CLASS.enabled} .${CLASS.compose}.${CLASS.composeOpen} button, body.${CLASS.enabled} .${CLASS.compose}.${CLASS.composeOpen} [role="button"], body.${CLASS.enabled} .${CLASS.compose}.${CLASS.composeOpen} label { min-height: 38px !important; touch-action: manipulation !important; } body.${CLASS.enabled} .${CLASS.history}.${CLASS.historyOpen} { position: fixed !important; z-index: 2147483601 !important; top: 0 !important; bottom: calc(68px + env(safe-area-inset-bottom)) !important; left: 0 !important; width: min(42vw, 160px) !important; min-width: 96px !important; height: auto !important; margin: 0 !important; padding: calc(56px + env(safe-area-inset-top)) 8px 16px 8px !important; box-sizing: border-box !important; overflow-y: auto !important; overflow-x: hidden !important; background: #f8f8fb !important; border-radius: 0 10px 10px 0 !important; box-shadow: 10px 0 28px rgba(0, 0, 0, .18) !important; transform: none !important; } body.${CLASS.enabled} .${CLASS.history}.${CLASS.historyOpen} img { max-width: 100% !important; height: auto !important; } body.${CLASS.enabled} .ant-select-dropdown, body.${CLASS.enabled} .ant-dropdown, body.${CLASS.enabled} .el-select-dropdown, body.${CLASS.enabled} [role="listbox"], body.${CLASS.enabled} [class*="dropdown" i], body.${CLASS.enabled} [class*="popover" i], body.${CLASS.enabled} [class*="popper" i] { z-index: 2147483608 !important; } `; if (typeof GM_addStyle === "function") { GM_addStyle(css); } else { const style = document.createElement("style"); style.textContent = css; document.documentElement.appendChild(style); } } })();