// ==UserScript==
// @name EdgeDL
// @namespace https://github.com/Chumor/EdgeDL
// @version 2.2.0-dev.cc23a1a
// @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
// @early-start
// ==/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 = `
`;
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();
})();