// ==UserScript== // @name Anywear 手机中文版与摄像头选择 // @namespace https://anywear.decart.ai/ // @version 0.1 // @description 自动全屏打开试穿,汉化 Anywear,支持摄像头选择、多件服装上传切换和截图保存。 // @license MIT // @match https://anywear.decart.ai/* // @run-at document-start // @grant none // @inject-into page // @sandbox raw // ==/UserScript== (function () { 'use strict'; const CAMERA_SELECT_ID = 'aw-zh-camera-select'; // 必须尽早运行:官方页面在点击“启用摄像头”时调用 getUserMedia, // 这里读取选择框并替换其写死的 facingMode: "user"。 function installCameraInterceptor() { const code = `(() => { if (window.__awZhCameraPatched) return; window.__awZhCameraPatched = true; const mediaDevices = navigator.mediaDevices; if (!mediaDevices || !mediaDevices.getUserMedia) return; const original = mediaDevices.getUserMedia.bind(mediaDevices); mediaDevices.getUserMedia = async function (constraints) { const next = constraints ? { ...constraints } : {}; const originalVideo = next.video; if (originalVideo !== false) { const video = originalVideo && typeof originalVideo === 'object' ? { ...originalVideo } : {}; const select = document.getElementById('${CAMERA_SELECT_ID}'); const choice = select ? select.value : 'front'; delete video.facingMode; delete video.deviceId; if (choice.startsWith('device:')) { video.deviceId = { exact: choice.slice(7) }; } else if (choice === 'back') { video.facingMode = { ideal: 'environment' }; } else { video.facingMode = { ideal: 'user' }; } next.video = video; } const stream = await original(next); window.dispatchEvent(new CustomEvent('aw-zh-camera-opened')); return stream; }; })();`; const script = document.createElement('script'); script.textContent = code; (document.documentElement || document).appendChild(script); script.remove(); } installCameraInterceptor(); const translations = new Map([ ['Try clothes on,', '试穿任意服装,'], ['anywhere.', '随时随地。'], ['Drag any clothing item from any online store and see it on you, instantly.', '从任意在线商店选择服装,即时查看上身效果。'], ['Add to Chrome', '添加到 Chrome'], ['Try now', '立即试穿'], ['Free during Beta.', 'Beta 期间免费'], ['Turn on your camera.', '打开摄像头'], ['Enable camera', '启用摄像头'], ['Starting…', '正在启动…'], ['Drag a product onto yourself.', '选择一件服装进行试穿'], ['For best results — remove any thick jacket or outer layer for the most accurate try-on.', '为获得更好效果,请先脱下厚外套或外层服装。'], ['Connecting…', '正在连接…'], ['Connecting to the studio…', '正在连接 AI 试衣间…'], ['Dressing…', '正在更换服装…'], ['Continue →', '继续 →'], ['Install on every online-store.', '在任意在线商店使用'], ['Add Anywear to Chrome and this exact try-on appears right on every product page — Zara, ASOS, Amazon and beyond. Drag any item onto yourself and see how it looks before you buy.', '将 Anywear 添加到 Chrome,即可在 Zara、ASOS、Amazon 等商品页面使用试穿功能。'], ['Try another outfit', '试穿另一件'], ['Own a store?', '拥有在线商店?'], ['Add virtual try-on to your website →', '将虚拟试穿添加到您的网站 →'], ['Privacy Policy', '隐私政策'], ['Terms of Service', '服务条款'], ['Camera permission denied. Enable it and retry.', '摄像头权限被拒绝,请授权后重试。'], ['Could not start. Please try again.', '无法启动,请重试。'], ['Connection issue — please try again.', '连接异常,请重试。'], ]); function translateText(root) { if (!root) return; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); for (const node of nodes) { const parent = node.parentElement; if (!parent || /^(SCRIPT|STYLE|TEXTAREA|CODE)$/i.test(parent.tagName)) continue; const trimmed = node.nodeValue.trim(); const translated = translations.get(trimmed); if (translated) node.nodeValue = node.nodeValue.replace(trimmed, translated); } } function cameraLabel(device, index) { if (device.label) return device.label; return `摄像头 ${index + 1}(授权后显示名称)`; } async function refreshCameraList(select) { if (!navigator.mediaDevices?.enumerateDevices) return; const previous = select.value; const devices = (await navigator.mediaDevices.enumerateDevices()) .filter(device => device.kind === 'videoinput'); select.querySelectorAll('option[data-device]').forEach(option => option.remove()); devices.forEach((device, index) => { const option = document.createElement('option'); option.value = `device:${device.deviceId}`; option.textContent = cameraLabel(device, index); option.dataset.device = 'true'; select.appendChild(option); }); if ([...select.options].some(option => option.value === previous)) { select.value = previous; } updateOutputMirroring(); } function updateOutputMirroring() { const select = document.getElementById(CAMERA_SELECT_ID); const output = document.getElementById('t-output'); if (!select || !output) return; // 前摄保留自拍镜像,后摄取消官网强制添加的镜像。 output.classList.toggle('aw-zh-no-mirror', isBackCameraSelected()); } function isBackCameraSelected() { const select = document.getElementById(CAMERA_SELECT_ID); if (!select) return false; const selectedText = select.selectedOptions?.[0]?.textContent || ''; return select.value === 'back' || /back|rear|environment|后置|背面/i.test(selectedText); } function flipImageHorizontally(dataUrl) { return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => { const canvas = document.createElement('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight; const context = canvas.getContext('2d'); context.translate(canvas.width, 0); context.scale(-1, 1); context.drawImage(image, 0, 0); resolve(canvas.toDataURL('image/png')); }; image.onerror = reject; image.src = dataUrl; }); } function addControls() { if (document.getElementById(CAMERA_SELECT_ID)) return; const host = document.querySelector('.cam-hero') || document.body; const controls = document.createElement('div'); controls.id = 'aw-zh-controls'; controls.innerHTML = ` 请在点击“启用摄像头”之前选择。 `; const style = document.createElement('style'); style.textContent = ` #aw-zh-controls { width:min(92%,420px); display:grid; gap:8px; color:#fff; font-family:inherit; } #aw-zh-controls label { font-size:15px; font-weight:600; } #aw-zh-controls select, #aw-zh-controls button { min-height:44px; border-radius:10px; border:1px solid rgba(255,255,255,.35); padding:0 12px; font-size:15px; } #aw-zh-controls select { color:#170404; background:#fff; } #aw-zh-controls button { color:#fff; background:rgba(255,255,255,.14); } #aw-zh-controls small { color:rgba(255,255,255,.75); } /* 将官网居中弹窗改成手机全屏试衣间。 */ #tryon-modal.tryon-modal { padding:0 !important; width:100vw; height:100dvh; } #tryon-modal .tryon-backdrop { display:none !important; } #tryon-modal .tryon-dialog { width:100vw !important; max-width:none !important; height:100dvh !important; max-height:none !important; border:0 !important; border-radius:0 !important; padding-top:max(22px,env(safe-area-inset-top)) !important; padding-bottom:max(22px,env(safe-area-inset-bottom)) !important; } #tryon-modal .tryon-close { display:none !important; } #tryon-modal .tryon-head { display:none !important; margin:0 !important; } #t-output.aw-zh-no-mirror { transform:none !important; } /* 隐藏官网自带的四件示例服装;节点仍保留给内部 setImage 调用。 */ #tstep2 .t-garments { display:none !important; } #tstep2 .t-tip { display:none !important; } #tstep2 .t-s2 { width:100% !important; } #tstep2 .t-cam-right { width:100% !important; flex:1 1 100% !important; } #aw-zh-closet { position:fixed; left:12px; right:12px; bottom:max(12px,env(safe-area-inset-bottom)); z-index:10010; display:flex; align-items:center; gap:8px; padding:8px; border-radius:14px; background:rgba(255,255,252,.96); box-shadow:0 8px 30px rgba(0,0,0,.28); backdrop-filter:blur(12px); } #aw-zh-upload { flex:0 0 auto; border:0; border-radius:999px; padding:11px 15px; background:#170404; color:#fff; font-size:14px; white-space:nowrap; } #aw-zh-capture { position:absolute; right:12px; bottom:12px; z-index:5; border:0; border-radius:999px; padding:10px 15px; background:#c65a92; color:#fff; box-shadow:0 4px 14px rgba(0,0,0,.28); font-family:inherit; font-size:14px; white-space:nowrap; cursor:pointer; } #aw-zh-garments { display:flex; flex:1; gap:7px; overflow-x:auto; overscroll-behavior-x:contain; scrollbar-width:thin; } .aw-zh-garment { position:relative; flex:0 0 54px; width:54px; height:68px; padding:0; overflow:hidden; border:2px solid transparent; border-radius:9px; background:#eee; } .aw-zh-garment.active { border-color:#c65a92; } .aw-zh-garment img { width:100%; height:100%; object-fit:cover; display:block; } .aw-zh-garment .remove { position:absolute; top:1px; right:1px; width:19px; height:19px; padding:0; border:0; border-radius:50%; background:rgba(0,0,0,.72); color:#fff; font-size:13px; line-height:19px; } `; document.head.appendChild(style); host.prepend(controls); const select = controls.querySelector(`#${CAMERA_SELECT_ID}`); select.addEventListener('change', updateOutputMirroring); controls.querySelector('#aw-zh-refresh-camera').addEventListener('click', () => refreshCameraList(select)); refreshCameraList(select).catch(console.warn); updateOutputMirroring(); } function addGarmentUpload() { if (document.getElementById('aw-zh-upload')) return; const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/jpeg,image/png,image/webp'; input.multiple = true; input.hidden = true; const closet = document.createElement('div'); closet.id = 'aw-zh-closet'; const button = document.createElement('button'); button.id = 'aw-zh-upload'; button.type = 'button'; button.textContent = '+添加衣服'; button.addEventListener('click', () => input.click()); const garmentList = document.createElement('div'); garmentList.id = 'aw-zh-garments'; garmentList.setAttribute('aria-label', '我的衣服'); const captureButton = document.createElement('button'); captureButton.id = 'aw-zh-capture'; captureButton.type = 'button'; captureButton.textContent = '截图保存'; async function captureCurrentFrame() { const video = document.getElementById('t-output'); if (!video || !video.videoWidth || !video.videoHeight || video.readyState < 2) { alert('AI 试穿画面尚未准备好,请稍后再截图。'); return; } const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const context = canvas.getContext('2d'); // 官网用 CSS 镜像视频;保存结果与用户屏幕上看到的方向保持一致。 const transform = getComputedStyle(video).transform; const mirrored = transform && transform !== 'none' && (/^matrix\(-1(?:,|\s)/.test(transform) || /^matrix3d\(-1(?:,|\s)/.test(transform)); if (mirrored) { context.translate(canvas.width, 0); context.scale(-1, 1); } context.drawImage(video, 0, 0, canvas.width, canvas.height); const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png', 1)); if (!blob) { alert('截图生成失败,请重试。'); return; } const stamp = new Date().toISOString().replace(/[:.]/g, '-'); const fileName = `anywear-${stamp}.png`; // 直接保存到浏览器的默认下载目录,不再打开系统分享面板。 const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); link.remove(); setTimeout(() => URL.revokeObjectURL(url), 30000); } captureButton.addEventListener('click', captureCurrentFrame); async function applyUploadedGarment(dataUrl, item) { const image = document.querySelector('#tstep2 .g-thumb img'); if (!image) { alert('请先点击“立即试穿”并启用摄像头。'); return; } document.querySelectorAll('.aw-zh-garment').forEach(el => el.classList.remove('active')); item.classList.add('active'); // 前摄输出采用自拍镜像,因此仅将送给模型的服装参考图水平翻转; // 用户衣柜里的原始缩略图保持不变。后摄直接发送原图。 image.src = isBackCameraSelected() ? dataUrl : await flipImageHorizontally(dataUrl); image.closest('.g-thumb')?.click(); } function addGarment(file) { const reader = new FileReader(); reader.onload = () => { const dataUrl = String(reader.result); const item = document.createElement('button'); item.type = 'button'; item.className = 'aw-zh-garment'; item.title = file.name; const preview = document.createElement('img'); preview.src = dataUrl; preview.alt = file.name; const remove = document.createElement('span'); remove.className = 'remove'; remove.textContent = '×'; remove.setAttribute('role', 'button'); remove.setAttribute('aria-label', `删除 ${file.name}`); remove.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); item.remove(); }); item.append(preview, remove); item.addEventListener('click', () => applyUploadedGarment(dataUrl, item)); garmentList.appendChild(item); }; reader.readAsDataURL(file); } input.addEventListener('change', () => { const files = [...(input.files || [])]; files.forEach(addGarment); // 清空后,同一批文件下次仍可再次选择。 input.value = ''; }); const cameraPanel = document.querySelector('#tstep2 .t-cam-panel'); if (cameraPanel) cameraPanel.appendChild(captureButton); else closet.appendChild(captureButton); closet.append(button, garmentList); document.body.append(input, closet); } function openTryOnAutomatically() { let opened = false; let attempts = 0; function forceOpen() { if (opened) return true; const modal = document.getElementById('tryon-modal'); if (!modal) return false; // 优先走官网逻辑;隔离环境中访问不到该函数时直接打开 DOM。 try { if (typeof window.openTryModal === 'function') window.openTryModal(); } catch (error) { console.warn('[Anywear 中文版] 官方打开函数不可用,改用直接打开。', error); } modal.classList.add('open'); modal.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; // 确保第一步可见,避免官网修改初始化顺序后出现空白全屏层。 const firstStep = document.getElementById('tstep1'); if (firstStep && !modal.querySelector('.tstep.active')) firstStep.classList.add('active'); opened = modal.classList.contains('open'); return opened; } if (forceOpen()) return; const timer = setInterval(() => { attempts += 1; if (forceOpen() || attempts >= 100) clearInterval(timer); }, 100); // 部分手机用户脚本管理器会延迟到 load/pageshow 阶段才注入。 window.addEventListener('load', forceOpen, { once: true }); window.addEventListener('pageshow', forceOpen, { once: true }); const observer = new MutationObserver(() => { if (forceOpen()) observer.disconnect(); }); observer.observe(document.documentElement, { childList: true, subtree: true }); } function initialize() { translateText(document.body); addControls(); addGarmentUpload(); openTryOnAutomatically(); const observer = new MutationObserver(records => { records.forEach(record => record.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) translateText(node); })); translateText(document.body); }); observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('aw-zh-camera-opened', () => { const select = document.getElementById(CAMERA_SELECT_ID); updateOutputMirroring(); if (select) refreshCameraList(select).catch(console.warn); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize, { once: true }); } else { initialize(); } })();