Force Mobile View (iOS Safari, XS Max hard mode)

Spoof mobile signals, enforce viewport, fix media queries, optional m. redirects

// ==UserScript==
// @name         Force Mobile View (iOS Safari, XS Max hard mode)
// @description  Spoof mobile signals, enforce viewport, fix media queries, optional m. redirects
// @namespace https://gf.qytechs.cn/users/1508709
// @version      1.0.0
// @author       https://github.com/sitien173
// @match        *://*/*
// @inject-into  page
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==
/* eslint-disable */
/* global GM_getValue, GM_setValue */

(function () {
  // ---- Config (tweak if you want) -----------------------------------------
  const MOBILE_UA =
    "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1";
  // iPhone XS Max CSS metrics (portrait): 414x896, DPR=3
  const CSS_WIDTH = 414;
  const CSS_HEIGHT = 896;
  const DPR = 3;

  const VIEWPORT_CONTENT =
    "width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1";

  const MOBILE_REDIRECTS = [
    { from: /^www\.youtube\.com$/i, to: "m.youtube.com" },
    { from: /^twitter\.com$/i, to: "mobile.twitter.com" },
    { from: /^www\.facebook\.com$/i, to: "m.facebook.com" },
    { from: /^en\.wikipedia\.org$/i, to: "m.wikipedia.org" },
    { from: /^www\.reddit\.com$/i, to: "m.reddit.com" },
  ];

  const host = location.hostname;
  const perSiteKey = (k) => `forceMobile:${host}:${k}`;
  const isRedirectEnabled = () => GM_getValue(perSiteKey("redirectEnabled"), true);
  const setRedirectEnabled = (v) => GM_setValue(perSiteKey("redirectEnabled"), !!v);

  if (typeof GM_registerMenuCommand === "function") {
    GM_registerMenuCommand(
      `[${isRedirectEnabled() ? "✓" : " "}] Mobile redirect on this site`,
      () => {
        setRedirectEnabled(!isRedirectEnabled());
        location.reload();
      }
    );
  }

  // ---- 0) Optional: auto-redirect common desktop → mobile hosts -----------
  (function maybeRedirect() {
    if (!isRedirectEnabled()) return;
    const rule = MOBILE_REDIRECTS.find((r) => r.from.test(host));
    if (rule && host.toLowerCase() !== rule.to.toLowerCase()) {
      const url = new URL(location.href);
      url.host = rule.to;
      location.replace(url.toString());
    }
  })();

  // ---- Helper to (attempt to) redefine read-only props where possible -----
  function defineRW(obj, prop, getter) {
    try {
      const d = Object.getOwnPropertyDescriptor(obj, prop);
      if (!d || d.configurable) {
        Object.defineProperty(obj, prop, { get: getter, configurable: true });
      }
    } catch (_) { /* ignore */ }
  }

  // ---- 1) Spoof mobile signals that many SPAs inspect ---------------------
  try {
    defineRW(Navigator.prototype, "userAgent", () => MOBILE_UA);
    defineRW(Navigator.prototype, "appVersion", () => MOBILE_UA);
    defineRW(Navigator.prototype, "platform", () => "iPhone");
    defineRW(Navigator.prototype, "vendor", () => "Apple Computer, Inc.");
    defineRW(Navigator.prototype, "maxTouchPoints", () => 5);

    // userAgentData (if present)
    if ("userAgentData" in Navigator.prototype) {
      defineRW(Navigator.prototype, "userAgentData", () => ({
        mobile: true,
        platform: "iOS",
        brands: [{ brand: "Safari", version: "17" }],
        toString() { return "[object NavigatorUAData]"; }
      }));
    }

    // Screen metrics & DPR
    defineRW(Screen.prototype, "width", () => CSS_WIDTH);
    defineRW(Screen.prototype, "height", () => CSS_HEIGHT);
    defineRW(window, "devicePixelRatio", () => DPR);

    // Legacy signals some libs still check
    try { Object.defineProperty(window, "orientation", { value: 0, configurable: true }); } catch (_) {}
    try { window.chrome ??= { runtime: {} }; } catch (_) {}
  } catch (_) { /* ignore */ }

  // ---- 2) Make CSS media queries think we're on touch / non-hover ---------
  try {
    const origMM = window.matchMedia.bind(window);
    window.matchMedia = function (q) {
      const query = String(q).trim().toLowerCase();
      // Force common mobile signals
      if (/\(hover:\s*none\)/.test(query) || /\(any-hover:\s*none\)/.test(query)) {
        return Object.assign(origMM(q), { matches: true });
      }
      if (/\(hover:\s*hover\)/.test(query) || /\(any-hover:\s*hover\)/.test(query)) {
        return Object.assign(origMM(q), { matches: false });
      }
      if (/\(pointer:\s*coarse\)/.test(query) || /\(any-pointer:\s*coarse\)/.test(query)) {
        return Object.assign(origMM(q), { matches: true });
      }
      if (/\(pointer:\s*fine\)/.test(query) || /\(any-pointer:\s*fine\)/.test(query)) {
        return Object.assign(origMM(q), { matches: false });
      }
      return origMM(q);
    };
  } catch (_) { /* ignore */ }

  // ---- 3) Aggressively enforce a mobile viewport --------------------------
  function setViewportMeta(node) {
    if (!node) return;
    node.setAttribute("name", "viewport");
    node.setAttribute("content", VIEWPORT_CONTENT);
  }

  function ensureViewport() {
    let vp = document.querySelector('meta[name="viewport"]');
    if (!vp) {
      vp = document.createElement("meta");
      setViewportMeta(vp);
      (document.head || document.documentElement).prepend(vp);
    } else {
      setViewportMeta(vp);
    }
  }

  // run ASAP…
  ensureViewport();

  // …and keep it enforced even if the site changes it later
  const headObs = new MutationObserver((muts) => {
    for (const m of muts) {
      if (m.type === "childList") {
        m.addedNodes.forEach((n) => {
          if (n.nodeType === 1 && n.matches?.('meta[name="viewport"]')) setViewportMeta(n);
        });
      }
      if (m.type === "attributes" && m.target?.matches?.('meta[name="viewport"]')) {
        setViewportMeta(m.target);
      }
    }
  });

  const startObs = () => {
    const head = document.head || document.documentElement;
    headObs.observe(head, { childList: true, subtree: true, attributes: true, attributeFilter: ["content", "name"] });
  };

  if (document.readyState === "loading") {
    startObs();
    document.addEventListener("DOMContentLoaded", ensureViewport, { once: true });
  } else {
    startObs();
    ensureViewport();
  }

  // ---- 4) Safety net: CSS to prevent desktop widths from leaking in -------
  const style = document.createElement("style");
  style.textContent = `
    html, body { max-width: 100vw !important; overflow-x: hidden !important; }
    /* Prevent frameworks from locking viewport to 980px etc. */
    body { width: auto !important; }
  `;
  (document.head || document.documentElement).appendChild(style);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址