Spaces:
Running on Zero
Running on Zero
| () => { | |
| /** | |
| * Dual-skin boot: pick desktop vs phone layout, keep a single set of IDs in the DOM. | |
| * | |
| * Rules (first match wins for force flags): | |
| * ?ui=phone | ?ui=mobile → phone | |
| * ?ui=desktop | ?ui=pc → desktop | |
| * Mobile / phone UA → phone (HF embeds often report a WIDE iframe width) | |
| * max-width ≤ 768px → phone | |
| * else → desktop | |
| * | |
| * Unused skin is removed so getElementById stays unambiguous for gallery.js. | |
| */ | |
| function wantPhoneUI() { | |
| try { | |
| var q = new URLSearchParams(window.location.search || ""); | |
| var force = (q.get("ui") || "").toLowerCase(); | |
| if (force === "phone" || force === "mobile") return true; | |
| if (force === "desktop" || force === "pc") return false; | |
| } catch (e) {} | |
| // Real phones/tablets in portrait-ish embeds: don't trust iframe CSS width alone | |
| try { | |
| var ua = navigator.userAgent || ""; | |
| if (/Android.+Mobile|iPhone|iPod|webOS|BlackBerry|IEMobile|Opera Mini/i.test(ua)) { | |
| return true; | |
| } | |
| // iPad / Android tablet — treat as phone layout (single column) | |
| if (/iPad|Android(?!.*Mobile)|Tablet/i.test(ua)) { | |
| return true; | |
| } | |
| if (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1) { | |
| return true; // iPadOS desktop UA | |
| } | |
| } catch (e2) {} | |
| try { | |
| if (window.matchMedia("(max-width: 768px)").matches) return true; | |
| // Very short + narrow-ish viewport (mobile browser chrome) | |
| if (window.matchMedia("(max-width: 900px) and (max-height: 900px) and (pointer: coarse)").matches) { | |
| return true; | |
| } | |
| } catch (e3) { | |
| if ((window.innerWidth || 1024) <= 768) return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Gradio / Spaces often inject maximum-scale=1 or user-scalable=no, which kills | |
| * pinch-zoom. Restore a viewport that allows zoom (a11y + your request). | |
| */ | |
| function ensureZoomableViewport() { | |
| try { | |
| var allow = | |
| "width=device-width, initial-scale=1, viewport-fit=cover, " + | |
| "minimum-scale=0.5, maximum-scale=5, user-scalable=yes"; | |
| var metas = document.querySelectorAll('meta[name="viewport"]'); | |
| if (!metas || !metas.length) { | |
| var m = document.createElement("meta"); | |
| m.setAttribute("name", "viewport"); | |
| m.setAttribute("content", allow); | |
| (document.head || document.documentElement).appendChild(m); | |
| return; | |
| } | |
| for (var i = 0; i < metas.length; i++) { | |
| metas[i].setAttribute("content", allow); | |
| } | |
| } catch (e) {} | |
| } | |
| /** | |
| * HF / Gradio wrap the app in transformed/overflow ancestors, so position:fixed | |
| * on .phone-run is relative to that box (often off-screen). Moving the bar to | |
| * document.body makes fixed = real viewport. Embeds often don't have those | |
| * ancestors — which is why floating Edit already worked there. | |
| */ | |
| function pinPhoneRunBar() { | |
| try { | |
| if (window.__qwenUiSkin !== "phone") return; | |
| var bar = document.querySelector(".phone-run"); | |
| if (!bar || !document.body) return; | |
| if (bar.parentNode === document.body) { | |
| bar.classList.add("phone-run--ported"); | |
| return; | |
| } | |
| // Marker so we can leave a spacer if needed (stack already has padding-bottom) | |
| bar.setAttribute("data-ported", "1"); | |
| bar.classList.add("phone-run--ported"); | |
| document.body.appendChild(bar); | |
| } catch (e) {} | |
| } | |
| function activate() { | |
| var phone = wantPhoneUI(); | |
| var desktop = document.getElementById("skin-desktop"); | |
| var phoneEl = document.getElementById("skin-phone"); | |
| // Skins not in DOM yet | |
| if (!desktop && !phoneEl) return false; | |
| // Always allow pinch/zoom — do not lock scale on phone or desktop | |
| ensureZoomableViewport(); | |
| if (phone) { | |
| if (desktop && desktop.parentNode) desktop.parentNode.removeChild(desktop); | |
| if (phoneEl) { | |
| phoneEl.hidden = false; | |
| phoneEl.removeAttribute("hidden"); | |
| phoneEl.style.display = ""; | |
| } | |
| document.documentElement.setAttribute("data-ui-skin", "phone"); | |
| try { document.body && document.body.setAttribute("data-ui-skin", "phone"); } catch (e4) {} | |
| window.__qwenUiSkin = "phone"; | |
| pinPhoneRunBar(); | |
| // DOM may still be settling on HF | |
| setTimeout(pinPhoneRunBar, 100); | |
| setTimeout(pinPhoneRunBar, 500); | |
| setTimeout(pinPhoneRunBar, 1500); | |
| } else { | |
| // If we previously ported the bar, remove orphan | |
| try { | |
| var orphan = document.querySelector("body > .phone-run"); | |
| if (orphan) orphan.parentNode.removeChild(orphan); | |
| } catch (e6) {} | |
| if (phoneEl && phoneEl.parentNode) phoneEl.parentNode.removeChild(phoneEl); | |
| if (desktop) { | |
| desktop.hidden = false; | |
| desktop.removeAttribute("hidden"); | |
| desktop.style.display = ""; | |
| } | |
| document.documentElement.setAttribute("data-ui-skin", "desktop"); | |
| try { document.body && document.body.setAttribute("data-ui-skin", "desktop"); } catch (e5) {} | |
| window.__qwenUiSkin = "desktop"; | |
| } | |
| return true; | |
| } | |
| // Expose for gallery.js / lora_demo.js if boot load order is flaky | |
| window.__qwenActivateUiSkin = activate; | |
| window.__qwenWantPhoneUI = wantPhoneUI; | |
| window.__qwenEnsureZoomableViewport = ensureZoomableViewport; | |
| window.__qwenPinPhoneRunBar = pinPhoneRunBar; | |
| function tryActivate(n) { | |
| // Fix zoom even before skins appear (Gradio may set a locked viewport early) | |
| ensureZoomableViewport(); | |
| if (activate()) { | |
| // Gradio sometimes rewrites viewport after load — re-assert zoom a few times | |
| setTimeout(ensureZoomableViewport, 200); | |
| setTimeout(ensureZoomableViewport, 1000); | |
| setTimeout(ensureZoomableViewport, 3000); | |
| return; | |
| } | |
| if (n < 60) setTimeout(function () { tryActivate(n + 1); }, 50); | |
| } | |
| tryActivate(0); | |
| } | |