// ==UserScript== // @name EdgeDL // @namespace https://github.com/Chumor/EdgeDL // @version 2.1.0-dev.6a0e721 // @description 让 Android Edge 支持调用外部下载器接管下载任务 // @icon https://cdn.jsdelivr.net/gh/Chumor/EdgeDL@main/assets/icons/edge.svg // @author Chumor // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_getResourceURL // @grant unsafeWindow // @license Apache-2.0 // @run-at document-start // @website https://scriptcat.org/zh-CN/script-show-page/5391 // @supportURL https://github.com/Chumor/EdgeDL/issues // @resource icon_idm https://cdn.jsdelivr.net/gh/Chumor/EdgeDL@main/assets/icons/idm.svg // @resource icon_idm_plus https://cdn.jsdelivr.net/gh/Chumor/EdgeDL@main/assets/icons/idm-plus.svg // @resource icon_adm https://cdn.jsdelivr.net/gh/Chumor/EdgeDL@main/assets/icons/adm.svg // @resource icon_abdm https://cdn.jsdelivr.net/gh/Chumor/EdgeDL@main/assets/icons/abdm.svg // @resource icon_fdm https://cdn.jsdelivr.net/gh/Chumor/EdgeDL@main/assets/icons/fdm.svg // @resource icon_edge https://cdn.jsdelivr.net/gh/Chumor/EdgeDL@main/assets/icons/edge.svg // ==/UserScript== (function () { 'use strict'; const DOWNLOADERS={IDM: 'idm.internet.download.manager',IDM_PLUS: 'idm.internet.download.manager.plus',ADM: 'com.dv.adm',ABDM: 'com.abdownloadmanager',FDM: 'org.freedownloadmanager.fdm',}; // 默认下载器 const DEFAULT_DOWNLOADER_KEY = 'edgedl-default-downloader'; // 版本信息 function getEdgeDLVersion() { return (typeof GM_info !== 'undefined' && GM_info.script?.version) ? GM_info.script.version : 'dev'; } const FALLBACK_ICON_BASE = 'https://cdn.jsdelivr.net/gh/Chumor/EdgeDL@main/assets/icons'; function getIconUrl(name, fallback) { try { if (typeof GM_getResourceURL === 'function') { return GM_getResourceURL(name); } } catch { // ignore } return fallback; } const downloaderIcons = { IDM: getIconUrl('icon_idm', `${FALLBACK_ICON_BASE}/idm.svg`), IDM_PLUS: getIconUrl('icon_idm_plus', `${FALLBACK_ICON_BASE}/idm-plus.svg`), ADM: getIconUrl('icon_adm', `${FALLBACK_ICON_BASE}/adm.svg`), ABDM: getIconUrl('icon_abdm', `${FALLBACK_ICON_BASE}/abdm.svg`), FDM: getIconUrl('icon_fdm', `${FALLBACK_ICON_BASE}/fdm.svg`), EDGE: getIconUrl('icon_edge', `${FALLBACK_ICON_BASE}/edge.svg`), }; async function showDownloadPicker(callback) { if (document.getElementById('edgedl-picker')) { callback(null); return; } const picker = document.createElement('div'); picker.id = 'edgedl-picker'; picker.classList.add('initializing'); const shadow = picker.attachShadow({ mode: 'open' }); shadow.innerHTML = `

选择下载器

EdgeDL v${getEdgeDLVersion()}
`; document.documentElement.appendChild(picker); const layoutPicker = () => { const vvp = window.visualViewport; const w = vvp ? vvp.width : document.documentElement.clientWidth; const card = shadow.querySelector('.edgedl-card'); if (card) card.style.maxWidth = (w - 32) + 'px'; }; layoutPicker(); if (window.visualViewport) { window.visualViewport.addEventListener('resize', layoutPicker); window.visualViewport.addEventListener('scroll', layoutPicker); } const style = document.createElement('style'); style.textContent = `:host{all:initial;position:fixed;inset:0;display:flex;justify-content:center;align-items:center;z-index:2147483647;pointer-events:none;contain:layout style paint;isolation:isolate;--edgedl-move-easing:cubic-bezier(0,0,0.2,1);--edgedl-fade-easing:cubic-bezier(0.4,0,0.2,1);--edgedl-exit-easing:cubic-bezier(0.4,0,1,1);--edgedl-enter-duration:300ms;--edgedl-fade-duration:200ms;--edgedl-exit-duration:200ms;}*,*::before,*::after{box-sizing:border-box;}.edgedl-bg{position:absolute;inset:0;background:rgba(0,0,0,0.42);backdrop-filter:blur(25px) saturate(140%);-webkit-backdrop-filter:blur(25px) saturate(140%);animation:edgedl-fade-in var(--edgedl-fade-duration) var(--edgedl-fade-easing) both;pointer-events:auto;will-change:opacity;}:host(.initializing) .edgedl-bg,:host(.initializing) .edgedl-card{pointer-events:none;user-select:none;cursor:wait;}.edgedl-card{position:relative;background:#fff;border-radius:24px;padding:20px;width:260px;max-width:100%;box-shadow:0 10px 28px rgba(0,0,0,0.25);display:flex;flex-direction:column;align-items:center;animation:edgedl-slide-up var(--edgedl-enter-duration) var(--edgedl-move-easing) both;transform-origin:center bottom;will-change:opacity,transform;pointer-events:auto;box-sizing:border-box;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-weight:400;line-height:1.4;-webkit-font-smoothing:antialiased;}h3{margin:8px 0 18px 0;font-weight:600;font-size:16px;color:#333;}.edgedl-version-tag{position:absolute;top:10px;right:12px;font-size:9px;transform:translate(0,0);font-family:ui-monospace,SFMono-Regular,monospace;color:#888;background:rgba(0,0,0,0.04);padding:2px 8px;border-radius:12px;font-weight:600;letter-spacing:0.3px;pointer-events:none;border:1px solid rgba(0,0,0,0.02);}.edgedl-options{display:flex;flex-direction:column;width:100%;gap:12px;}.edgedl-options button{display:flex;align-items:center;gap:10px;padding:10px;border:none;border-radius:12px;background:#F0F0F0;font-weight:500;cursor:pointer;transition:background 0.2s;}.edgedl-options button:hover{background:#e0e0e0;}.edgedl-options img{width:24px;height:24px;}.edgedl-options button.selected{background:rgba(76,175,80,0.12);outline:1.5px solid rgba(76,175,80,0.72);}@media (prefers-color-scheme:dark){.edgedl-card{background:#292929;color:#FFFFFF;box-shadow:0 4px 16px rgba(0,0,0,0.6);}h3{color:#F5F5F5;}.edgedl-options button{background:#383838;color:#FFFFFF;}#edgedl-picker .edgedl-options button.selected{background:rgba(129,199,132,0.16);outline-color:rgba(129,199,132,0.82);}}@keyframes edgedl-fade-in{from{opacity:0;}to{opacity:1;}}@keyframes edgedl-slide-up{from{opacity:0;transform:translateY(24px) scale(0.96);}to{opacity:1;transform:translateY(0) scale(1);}}@keyframes edgedl-fade-out{from{opacity:1;}to{opacity:0;}}@keyframes edgedl-slide-down{from{opacity:1;transform:translateY(0) scale(1);}to{opacity:0;transform:translateY(12px) scale(0.98);}}:host(.closing) .edgedl-bg{animation:edgedl-fade-out var(--edgedl-exit-duration) var(--edgedl-exit-easing) forwards;}:host(.closing) .edgedl-card{animation:edgedl-slide-down var(--edgedl-exit-duration) var(--edgedl-exit-easing) forwards;}@media (prefers-reduced-motion:reduce){.edgedl-bg,.edgedl-card,:host(.closing) .edgedl-bg,:host(.closing) .edgedl-card{animation-duration:1ms;}}`; shadow.appendChild(style); // 读取默认下载器 const defaultDownloader = await GM_getValue(DEFAULT_DOWNLOADER_KEY, null); const defaultCheckbox = shadow.querySelector('#edgedl-set-default'); if (defaultCheckbox) defaultCheckbox.checked = !!defaultDownloader; if (defaultDownloader) { // 高亮默认下载器按钮 const defaultBtn = shadow.querySelector(`button[data-pkg="${defaultDownloader}"]`); if (defaultBtn) defaultBtn.classList.add('selected'); } // 取消勾选时清除默认下载器 defaultCheckbox.addEventListener('change', async () => { if (!defaultCheckbox.checked) { await GM_deleteValue(DEFAULT_DOWNLOADER_KEY); shadow.querySelector('button.selected')?.classList.remove('selected'); } }); // 点击唤起 shadow.querySelectorAll('button').forEach(btn => { btn.addEventListener('click', async () => { const pkg = btn.dataset.pkg || ''; // 按当前选择更新默认下载器 if (pkg === 'edge') { await GM_deleteValue(DEFAULT_DOWNLOADER_KEY); } else if (defaultCheckbox.checked) { await GM_setValue(DEFAULT_DOWNLOADER_KEY, pkg); } callback(pkg); gotoClose(false); }); }); picker.classList.remove('initializing'); function gotoClose(cancelled = true) { if (picker.classList.contains('closing')) return; if (cancelled) callback(null); picker.classList.add('closing'); let removed = false; const removePicker = () => { if (removed) return; removed = true; if (window.visualViewport) { window.visualViewport.removeEventListener('resize', layoutPicker); window.visualViewport.removeEventListener('scroll', layoutPicker); } picker.remove(); window.dispatchEvent(new CustomEvent('edgedl:picker-closed')); }; const card = shadow.querySelector('.edgedl-card'); const onAnimationEnd = (event) => { if (event.target !== card) return; card.removeEventListener('animationend', onAnimationEnd); removePicker(); }; card?.addEventListener('animationend', onAnimationEnd); window.setTimeout(removePicker, 260); } shadow.querySelector('.edgedl-bg')?.addEventListener('click', () => gotoClose()); } function buildIntentUrl(url, packageName) { const scheme = url.startsWith('https') ? 'https' : 'http'; const path = url.replace(/^https?:\/\//, ''); return `intent://${path}#Intent;scheme=${scheme};package=${packageName};type=*/*;action=android.intent.action.VIEW;category=android.intent.category.BROWSABLE;end`; } function openDownloader(url, type) { if (!url || typeof url !== 'string') { throw new Error('Invalid URL'); } const packageName = DOWNLOADERS[type]; if (!packageName) { throw new Error(`Unknown downloader type:${type}`); } const intentUrl = buildIntentUrl(url, packageName); window.location.href = intentUrl; } let styleInjected = false; let activeToast = null; // 注入全局样式 function injectStyle() { if (styleInjected) return; styleInjected = true; const style = document.createElement('style'); style.textContent = `.edgedl-toast{position:fixed;top:64px;right:12px;left:auto;bottom:auto;transform:translateX(20px);z-index:999999;pointer-events:none;font-size:13px;font-weight:500;line-height:1.4;padding:7px 14px;border-radius:10px;white-space:nowrap;opacity:0;transition:opacity .22s ease,transform .22s ease;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);}.edgedl-toast[data-theme="light"]{background:#333333;color:#fff;box-shadow:0 4px 10px rgba(0,0,0,.25);}.edgedl-toast[data-theme="dark"]{background:#383838;color:#f5f5f5;border:1px solid rgba(255,255,255,.08);box-shadow:0 6px 16px rgba(0,0,0,.6);}.edgedl-toast.show{opacity:1;transform:translateX(0);}.edgedl-toast[data-type="error"]::before{content:"⚠";margin-right:6px;color:#f87171;}.edgedl-toast[data-type="info"]::before{content:"ⓘ";margin-right:6px;opacity:.8;}`; document.head.appendChild(style); } function getTheme() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } function showToast(message, options = {}) { try { injectStyle(); const finalOptions = typeof options === 'number' ? { duration: options } : options; const { duration = 900, type = 'info' } = finalOptions; if (activeToast) activeToast.remove(); const toast = document.createElement('div'); toast.className = 'edgedl-toast'; toast.dataset.theme = getTheme(); toast.dataset.type = type; toast.textContent = message; document.body.appendChild(toast); activeToast = toast; requestAnimationFrame(() => { toast.classList.add('show'); }); setTimeout(() => { toast.classList.remove('show'); toast.addEventListener('transitionend', () => { toast.remove(); if (activeToast === toast) activeToast = null; }, { once: true }); }, duration); } catch (err) { console.warn('Toast 创建失败', err); } } async function openDownload(url, downloader) { const launcherKey = Object.keys(DOWNLOADERS) .find((key) => DOWNLOADERS[key] === downloader); if (launcherKey) { showToast(`${launcherKey} 正在唤起`); openDownloader(url, launcherKey); return; } showToast('Edge 内置下载'); setTimeout(() => { window.location.href = url; }, 0); } async function requestDownload(url) { if (!url) return; const dl = await GM_getValue(DEFAULT_DOWNLOADER_KEY); if (dl) { return openDownload(url, dl); } const selected = await new Promise((resolve) => { showDownloadPicker(resolve); }); if (selected) { return openDownload(url, selected); } return null; } // 下载文件后缀匹配 const EXTENSIONS=['.apk', '.apks', '.xapk', '.apkm', '.ipa', '.obb', '.aab','.zip', '.rar', '.7z', '.tar', '.gz', '.tgz', '.bz2', '.xz','.iso', '.cab', '.jar', '.z','.mp4', '.mkv', '.avi', '.mov', '.flv', '.wmv', '.webm','.m4v', '.3gp', '.ts', '.mpg', '.mpeg', '.vob','.mp3', '.flac', '.wav', '.ogg', '.m4a', '.aac', '.wma', '.ape','.pdf', '.epub', '.mobi', '.azw3', '.djvu','.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx','.exe', '.msi', '.bin', '.dat', '.dmg', '.bat', '.sh', '.img','.torrent']; // 下载链接特征匹配 const KEYWORDS=['/down/', '/download/', '/downloads/', '/dl/', '/fetch/','/files/', '/file/', '/attach/', '/attachment/', '/media/', '/static/','/assets/', '/cdn/', '/dist/', '/repo/', '/backup/', '/upload/','/releases/download/', '/binary/', '/pkg/','?file=', '&file=', '?filename=', '&filename=','download?', '&download=', '?download=','force_download', 'response-content-disposition=', 'content-disposition=attachment']; // 非下载页面排除规则 const EXCLUDE_PATHS = /\/(login|reg(ister)?|sign(in|up|out)|logout|account|user|blob|src|tree)\//i; // 下载跳转页匹配 const REDIRECT_DOWNLOAD_PAGES = [ // 腾讯游戏 /\/zlkdatasys\/mct\/(?:d\/[^/?#]+|proj_\d+\/download)\.shtml(?:[?#].*)?$/i, // 豌豆荚、九游 /\/game\/downs_\d+_\d+\.html(?:[?#].*)?$/i, // 应用宝 /\/\/sj\.qq\.com\/appdetail\/[^/?#]+(?:[?#].*)?$/i, /\/\/sj\.qq\.com\/myapp\/detail\.htm\?[^#]*(?:apkname|pkgname)=/i, ]; // 下载链接检测 function isDownloadLink(url) { if (url?.includes('sourceforge.net/projects/') && url.includes('/files/')) return false; if (!url || !url.startsWith('http')) return false; const lowerUrl = url.toLowerCase(); // 排除非下载页面 if (EXCLUDE_PATHS.test(lowerUrl)) return false; // 排除类 Unix 目录路径 if (/\/data\/data\/[^?#]*$/i.test(lowerUrl)) return false; // 下载跳转页匹配 if (REDIRECT_DOWNLOAD_PAGES.some(pattern => pattern.test(lowerUrl))) return true; // 后缀匹配 try { const path = new URL(url).pathname.toLowerCase(); if (EXTENSIONS.some(ext => path.endsWith(ext))) return true; } catch { const path = lowerUrl.split('?')[0].split('#')[0]; if (EXTENSIONS.some(ext => path.endsWith(ext))) return true; } // 关键字匹配 return KEYWORDS.some(kw => lowerUrl.includes(kw)); } function extractUrlFromOnclick(onclick) { if (!onclick) return null; const URL_PATTERN = /(https?:\/\/[^"'()\s]+)/i; const match = onclick.match(URL_PATTERN); if (match) { return match[1]; } return null; } let interceptEnabled = true; let downloadGestureUntil = 0; let bridgeAttached = false; function getPageWindow() { return (globalThis.unsafeWindow || window); } function isInvalidNavigationUrl(url) { const value = url.trim().toLowerCase(); return !value || value === '#' || value === '##' || value.startsWith('javascript:'); } function normalizeUrl(input) { if (!input) return ''; try { const url = new URL(String(input), location.href).href; return url.startsWith('http') ? url : ''; } catch { return ''; } } function tryInterceptNavigation(input) { if (!interceptEnabled) return false; const url = normalizeUrl(input); const inDownloadGesture = Date.now() <= downloadGestureUntil; if (!url || (!isDownloadLink(url) && !inDownloadGesture)) return false; downloadGestureUntil = 0; void requestDownload(url); return true; } const KEY = 'edgedl-site-intercept'; // 获取接管站点列表 async function getInterceptSites() { const list = await GM_getValue(KEY, []); return Array.isArray(list) ? list : []; } // 保存接管站点列表 async function setInterceptSites(list) { const normalized = list.map((i) => typeof i === 'string' ? i.toLowerCase() : String(i).toLowerCase()); await GM_setValue(KEY, normalized); interceptEnabled = !normalized.includes(location.hostname.toLowerCase()); return normalized; } // 判断当前站点是否已跳过接管 async function isSiteIntercepted() { const host = location.hostname.toLowerCase(); const list = await getInterceptSites(); if (list.length === 0) return false; return list.some(item => item.toLowerCase() === host); } // 切换当前站点接管状态 async function toggleSiteIntercept() { const host = location.hostname.toLowerCase(); const list = await getInterceptSites(); let added; const index = list.findIndex(item => item.toLowerCase() === host); if (index >= 0) { list.splice(index, 1); added = false; } else { list.push(host); added = true; } await setInterceptSites(list); return added; } // 挂载点击拦截器 function attachClickInterceptor() { document.addEventListener('click', handleClick, true); } function handleClick(e) { // @ts-ignore: 自定义属性拦截 if (e._edgedl_handled) return; // @ts-ignore e._edgedl_handled = true; const target = e.target; if (target?.closest?.('label.hope-checkbox, .hope-checkbox, .hope-checkbox__control, input[type="checkbox"]')) return; const downloadTrigger = target?.closest?.('[class*="download" i], [id*="download" i], [dt-eid*="download" i]'); if (downloadTrigger) { downloadGestureUntil = Date.now() + 1500; } const link = target?.closest?.('a, [onclick], [data-ng-href], [data-href], [data-url]'); let url = ''; if (link) { url = link.getAttribute('href') || link.getAttribute('data-ng-href') || link.getAttribute('data-href') || link.getAttribute('data-url') || link.href || ''; } if (isInvalidNavigationUrl(url)) { const onclick = link ? link.getAttribute('onclick') || link.closest('[onclick]')?.getAttribute('onclick') : target?.closest?.('[onclick]')?.getAttribute('onclick'); if (onclick) { url = extractUrlFromOnclick(onclick) || ''; } } if (isInvalidNavigationUrl(url) && downloadTrigger && isDownloadLink(location.href)) { url = location.href; } else { url = normalizeUrl(url); } if (!url || !isDownloadLink(url)) return; // 命中接管排除策略:跳过 EdgeDL 接管并提示,交还浏览器默认行为 if (!interceptEnabled) { showToast('已跳过接管', { type: 'info', duration: 1500 }); return; } // 阻止浏览器原生下载与页面跳转 e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); void requestDownload(url); } // 接管页面脚本触发的下载跳转 function attachPageBridgeInterceptor() { if (bridgeAttached) return; bridgeAttached = true; attachClickInterceptor(); void isSiteIntercepted().then((value) => { interceptEnabled = !value; }); const pageWindow = getPageWindow(); const originalOpen = pageWindow.open; pageWindow.open = function patchedOpen(url, target, features) { if (tryInterceptNavigation(url)) return null; return originalOpen.call(pageWindow, url, target, features); }; try { const originalClick = pageWindow.HTMLAnchorElement.prototype.click; pageWindow.HTMLAnchorElement.prototype.click = function patchedClick() { if (tryInterceptNavigation(this.href)) return; return originalClick.call(this); }; } catch { } ['assign', 'replace'].forEach((method) => { try { const original = pageWindow.Location.prototype[method]; pageWindow.Location.prototype[method] = function patchedLocation(url) { if (tryInterceptNavigation(url)) return; return original.call(this, url); }; } catch { } }); } let menuRegistered = false; function registerMenu() { if (menuRegistered) return; if (typeof GM_registerMenuCommand !== 'function') return; // 更改默认下载器 GM_registerMenuCommand('更改默认下载器', () => { showDownloadPicker(() => { }); }); // 切换站点接管状态 GM_registerMenuCommand('切换本站接管状态', async () => { const added = await toggleSiteIntercept(); showToast(added ? '已禁止接管本站' : '已允许接管本站', { type: 'info', duration: 1500 }); }); menuRegistered = true; } function initDeepSeekHandler() { if (!location.hostname.includes('download.deepseek.com')) return; const APK_URL = "https://download.deepseek.com/apk/deepseek.apk"; const takeover = (e) => { if (!e || e.defaultPrevented) return; e.preventDefault?.(); e.stopPropagation?.(); e.stopImmediatePropagation?.(); requestDownload(APK_URL); return false; }; document.addEventListener('click', e => { const target = e.target; if (target?.closest?.('div')?.textContent?.includes('下载 APK 文件')) takeover(e); }, true); } // 初始化下载接管 function init() { attachPageBridgeInterceptor(); registerMenu(); initDeepSeekHandler(); } init(); })();