您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Redirect Discord links to LiveContainer so they open in the containerized Discord app on iOS.
// ==UserScript== // @name Discord → LiveContainer (iOS) // @namespace sharmanhall // @version 0.5 // @description Redirect Discord links to LiveContainer so they open in the containerized Discord app on iOS. // @author sharmanhall // @match https://discord.com/* // @match https://ptb.discord.com/* // @match https://canary.discord.com/* // @match https://discordapp.com/* // @match https://ptb.discordapp.com/* // @match https://canary.discordapp.com/* // @match https://discord.gg/* // @match *://*/* // @grant none // @license MIT // @run-at document-start // @noframes // ==/UserScript== (function () { 'use strict'; // ---- prefs ---- const VERBOSE = true; // set false to quiet logs const AUTO_REDIRECT_ON_DISCORD_PAGES = true; // when you're *on* a Discord page const REWRITE_LINKS_ON_ALL_PAGES = true; // rewrite <a> that point to Discord anywhere const ADD_LC_FLAG = true; // append lc=1 to avoid bounce loops const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); // First-party web/app domains. Intentionally NOT including CDN/media hosts. const discordHosts = new Set([ 'discord.com', 'discordapp.com', 'ptb.discord.com', 'canary.discord.com', 'ptb.discordapp.com', 'canary.discordapp.com', 'discord.gg' // invite links ]); // Hosts we explicitly ignore (CDN/media/attachments, image proxies, etc.). const discordExcludeHosts = new Set([ 'cdn.discordapp.com', 'media.discordapp.net', 'images-ext-1.discordapp.net', 'images-ext-2.discordapp.net' ]); function log(...args){ if (VERBOSE) console.log('[LC-Discord]', ...args); } function isDiscordURL(u) { try { const url = (u instanceof URL) ? u : new URL(u, location.href); if (discordExcludeHosts.has(url.hostname)) return false; return [...discordHosts].some(h => url.hostname === h || url.hostname.endsWith('.' + h)); } catch { return false; } } // Encode for LiveContainer's open-web-page scheme (expects Base64) function toBase64(str) { try { return btoa(String.fromCharCode(...new TextEncoder().encode(str))); } catch { // Fallback for older Safari return btoa(unescape(encodeURIComponent(str))); } } function buildLcUrl(originalUrl) { const url = new URL(originalUrl, location.href); if (ADD_LC_FLAG && !url.searchParams.has('lc')) url.searchParams.set('lc', '1'); return `livecontainer://open-web-page?url=${toBase64(url.toString())}`; } function redirectToLC(u) { const lc = buildLcUrl(u); log('Redirecting to LiveContainer:', lc); location.replace(lc); // avoid extra history entries } // 1) If we're on a Discord page already, bounce to LiveContainer (iOS only) if (isIOS && AUTO_REDIRECT_ON_DISCORD_PAGES && isDiscordURL(location.href)) { const cur = new URL(location.href); if (!cur.searchParams.get('lc')) { redirectToLC(location.href); return; // stop executing further on this page } else { log('lc=1 present; skipping auto-redirect to avoid loop.'); } } if (!isIOS) return; // iOS-only behavior below // Prefer any expanded URL attributes if present and first-party. function resolveTargetHref(a) { const expanded = a?.dataset?.expandedUrl || a?.getAttribute?.('data-expanded-url') || a?.dataset?.fullUrl || a?.getAttribute?.('data-full-url'); if (expanded && isDiscordURL(expanded)) return expanded; return a?.href || ''; } // 2) Global capture fallback for delegated clicks (robust vs dynamic UIs) if (REWRITE_LINKS_ON_ALL_PAGES) { document.addEventListener('click', (e) => { const a = e.target?.closest?.('a[href]'); if (!a) return; const targetHref = resolveTargetHref(a); if (!isDiscordURL(targetHref)) return; try { e.preventDefault(); e.stopPropagation(); redirectToLC(targetHref); } catch (err) { log('Error redirecting (global):', err); } }, { capture: true, passive: false }); } // 3) Per-anchor hook + dynamic observer (in case sites block global handlers) if (REWRITE_LINKS_ON_ALL_PAGES) { const processAnchor = (a) => { if (!a || !a.href) return; if (a.dataset.lcDiscord === '1') return; // double-hook guard const targetHref = resolveTargetHref(a); if (!isDiscordURL(targetHref)) return; const handler = (e) => { try { e.preventDefault(); e.stopPropagation(); redirectToLC(targetHref); } catch (err) { log('Error redirecting (anchor):', err); } }; a.addEventListener('click', handler, { capture: true, passive: false }); a.dataset.lcDiscord = '1'; log('Hooked Discord link:', targetHref); }; // Initial pass document.querySelectorAll('a[href]:not([data-lc-discord])').forEach(processAnchor); // Observe dynamically-added links (SPAs, infinite scroll, etc.) const mo = new MutationObserver((muts) => { for (const m of muts) { for (const node of m.addedNodes) { if (node.nodeType !== 1) continue; if (node.tagName === 'A' && node.href) processAnchor(node); else node.querySelectorAll?.('a[href]:not([data-lc-discord])').forEach(processAnchor); } } }); mo.observe(document.documentElement, { childList: true, subtree: true }); } })();