Updated all documentation, configuration, and code references from 'Xget for Chrome' to 'Xget for Chromium' for consistency with supported browsers. This includes issue templates, workflow files, privacy and security policies, source code logs, manifest, and popup UI.
388 lines
9.8 KiB
JavaScript
388 lines
9.8 KiB
JavaScript
// 内容脚本,在支持的平台上注入下载拦截功能
|
||
|
||
// 平台检测和 URL 转换
|
||
const PLATFORMS = {
|
||
gh: {
|
||
base: "https://github.com",
|
||
name: "GitHub",
|
||
pattern: /^https:\/\/github\.com\//,
|
||
},
|
||
gl: {
|
||
base: "https://gitlab.com",
|
||
name: "GitLab",
|
||
pattern: /^https:\/\/gitlab\.com\//,
|
||
},
|
||
hf: {
|
||
base: "https://huggingface.co",
|
||
name: "Hugging Face",
|
||
pattern: /^https:\/\/huggingface\.co\//,
|
||
},
|
||
};
|
||
|
||
// 初始化内容脚本
|
||
(async function () {
|
||
console.log("Xget for Chromium:内容脚本已加载");
|
||
|
||
// 监听来自后台脚本的消息
|
||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||
if (request.action === "showNotification") {
|
||
showNotification(request.message, request.showRefreshButton);
|
||
sendResponse({ success: true });
|
||
}
|
||
});
|
||
|
||
// 检查扩展是否已启用并配置
|
||
const settings = await getSettings();
|
||
if (!settings.enabled || !settings.xgetDomain) {
|
||
return;
|
||
}
|
||
|
||
// 找到当前平台
|
||
const currentPlatform = detectPlatform(window.location.href);
|
||
if (!currentPlatform || !settings.enabledPlatforms[currentPlatform]) {
|
||
return;
|
||
}
|
||
|
||
// 添加下载拦截
|
||
interceptDownloadLinks();
|
||
|
||
// 监控动态添加的内容
|
||
observePageChanges();
|
||
})();
|
||
|
||
async function getSettings() {
|
||
try {
|
||
return await new Promise((resolve) => {
|
||
chrome.runtime.sendMessage({ action: "getSettings" }, resolve);
|
||
});
|
||
} catch (error) {
|
||
console.error("获取设置时出错:", error);
|
||
return { enabled: false };
|
||
}
|
||
}
|
||
|
||
function detectPlatform(url) {
|
||
for (const [key, platform] of Object.entries(PLATFORMS)) {
|
||
if (platform.pattern.test(url)) {
|
||
return key;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function interceptDownloadLinks() {
|
||
// 拦截下载链接的点击事件
|
||
document.addEventListener(
|
||
"click",
|
||
async (event) => {
|
||
const link = event.target.closest("a");
|
||
if (!link || !link.href) return;
|
||
|
||
// 检查这是否是下载链接
|
||
if (isDownloadLink(link)) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
|
||
await handleDownloadLink(link.href);
|
||
}
|
||
},
|
||
true
|
||
);
|
||
}
|
||
|
||
function isDownloadLink(link) {
|
||
const href = link.href.toLowerCase();
|
||
const url = new URL(link.href);
|
||
const pathname = url.pathname.toLowerCase();
|
||
|
||
// 第一检查:明确的下载属性
|
||
if (link.download || link.hasAttribute("download")) {
|
||
return true;
|
||
}
|
||
|
||
// 第二检查:表示可下载文件的文件扩展名
|
||
const fileExtensions = [
|
||
".zip",
|
||
".tar.gz",
|
||
".tar.bz2",
|
||
".tar.xz",
|
||
".7z",
|
||
".rar",
|
||
".gz",
|
||
".bz2",
|
||
".exe",
|
||
".msi",
|
||
".dmg",
|
||
".pkg",
|
||
".deb",
|
||
".rpm",
|
||
".apk",
|
||
".jar",
|
||
".war",
|
||
".ear",
|
||
".iso",
|
||
".img",
|
||
".pdf",
|
||
".doc",
|
||
".docx",
|
||
".xls",
|
||
".xlsx",
|
||
".ppt",
|
||
".pptx",
|
||
".mp4",
|
||
".avi",
|
||
".mkv",
|
||
".mov",
|
||
".wmv",
|
||
".flv",
|
||
".mp3",
|
||
".wav",
|
||
".flac",
|
||
".ogg",
|
||
".jpg",
|
||
".jpeg",
|
||
".png",
|
||
".gif",
|
||
".bmp",
|
||
".tiff",
|
||
".svg",
|
||
".whl",
|
||
".egg",
|
||
".gem",
|
||
".nupkg",
|
||
];
|
||
|
||
// 检查 URL 是否以文件扩展名结尾
|
||
if (fileExtensions.some((ext) => pathname.endsWith(ext))) {
|
||
return true;
|
||
}
|
||
|
||
// 第三检查:GitHub 特定模式
|
||
const allowedGitHubHosts = ["github.com"];
|
||
try {
|
||
const parsedUrl = new URL(href);
|
||
if (allowedGitHubHosts.includes(parsedUrl.host)) {
|
||
// GitHub 发布资源下载 URL 遵循模式:/releases/download/
|
||
if (pathname.includes("/releases/download/")) {
|
||
return true;
|
||
}
|
||
// GitHub 存档下载 URL
|
||
if (
|
||
pathname.includes("/archive/") &&
|
||
(pathname.endsWith(".zip") || pathname.endsWith(".tar.gz"))
|
||
) {
|
||
return true;
|
||
}
|
||
// GitHub 原始文件 URL - 新增:支持原始文件链接
|
||
if (pathname.includes("/raw/")) {
|
||
return true;
|
||
}
|
||
// 排除导航到发布页面(仅 /releases 或 /releases/)
|
||
if (pathname.endsWith("/releases") || pathname.endsWith("/releases/")) {
|
||
return false;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error("无效的 URL:", href, e);
|
||
}
|
||
|
||
// 第四检查:GitLab 特定模式
|
||
const allowedGitLabHosts = ["gitlab.com"];
|
||
try {
|
||
const parsedUrl = new URL(href);
|
||
if (allowedGitLabHosts.includes(parsedUrl.host)) {
|
||
// GitLab 存档下载
|
||
if (pathname.includes("/-/archive/")) {
|
||
return true;
|
||
}
|
||
// GitLab 发布下载
|
||
if (
|
||
pathname.includes("/-/releases/") &&
|
||
pathname.includes("/downloads/")
|
||
) {
|
||
return true;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error("无效的 URL:", href, e);
|
||
}
|
||
|
||
// 第五检查:Hugging Face 文件下载
|
||
const allowedHuggingFaceHosts = ["huggingface.co"];
|
||
try {
|
||
const parsedUrl = new URL(href);
|
||
if (allowedHuggingFaceHosts.includes(parsedUrl.host)) {
|
||
// HF 文件下载 URL 包含 /resolve/
|
||
if (pathname.includes("/resolve/")) {
|
||
return true;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error("无效的 URL:", href, e);
|
||
}
|
||
|
||
// 第七检查:明确的下载文本指示器(更具体)
|
||
const downloadTextIndicators = ["download", "download file", "get file"];
|
||
const linkText = link.textContent.toLowerCase().trim();
|
||
if (
|
||
downloadTextIndicators.some(
|
||
(indicator) =>
|
||
linkText === indicator || linkText.startsWith(indicator + " ")
|
||
)
|
||
) {
|
||
return true;
|
||
}
|
||
|
||
// 默认:不是下载链接
|
||
return false;
|
||
}
|
||
|
||
async function handleDownloadLink(url) {
|
||
try {
|
||
const settings = await getSettings();
|
||
if (!settings.enabled || !settings.xgetDomain) return;
|
||
|
||
const transformedUrl = transformUrl(url, settings);
|
||
if (transformedUrl) {
|
||
// 显示通知
|
||
showNotification(`下载已通过 Xget 重定向`);
|
||
|
||
// 触发下载
|
||
window.location.href = transformedUrl;
|
||
}
|
||
} catch (error) {
|
||
console.error("处理下载时出错:", error);
|
||
}
|
||
}
|
||
|
||
function transformUrl(url, settings) {
|
||
try {
|
||
const platform = detectPlatform(url);
|
||
if (!platform || !settings.enabledPlatforms[platform]) {
|
||
return null;
|
||
}
|
||
|
||
const urlObj = new URL(url);
|
||
const path = urlObj.pathname + urlObj.search + urlObj.hash;
|
||
|
||
return `https://${settings.xgetDomain}/${platform}${path}`;
|
||
} catch (error) {
|
||
console.error("转换 URL 时出错:", error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function observePageChanges() {
|
||
const observer = new MutationObserver(() => {
|
||
// 监控页面变化(无需额外操作)
|
||
});
|
||
|
||
observer.observe(document.body, {
|
||
childList: true,
|
||
subtree: true,
|
||
});
|
||
}
|
||
|
||
function showNotification(message, showRefreshButton = false) {
|
||
try {
|
||
// 首先删除所有现有通知
|
||
const existingNotifications =
|
||
document.querySelectorAll(".xget-notification");
|
||
existingNotifications.forEach((notification) => notification.remove());
|
||
|
||
// 创建一个简单的通知
|
||
const notification = document.createElement("div");
|
||
notification.className = "xget-notification";
|
||
|
||
// 创建消息容器
|
||
const messageDiv = document.createElement("div");
|
||
messageDiv.textContent = message;
|
||
messageDiv.style.marginBottom = showRefreshButton ? "8px" : "0";
|
||
notification.appendChild(messageDiv);
|
||
|
||
// 如果需要,添加刷新按钮
|
||
if (showRefreshButton) {
|
||
const refreshButton = document.createElement("button");
|
||
refreshButton.textContent = "🔄 刷新页面";
|
||
refreshButton.style.cssText = `
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||
color: white;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
margin-top: 4px;
|
||
width: 100%;
|
||
transition: background 0.2s;
|
||
`;
|
||
|
||
refreshButton.addEventListener("mouseenter", () => {
|
||
refreshButton.style.background = "rgba(255, 255, 255, 0.3)";
|
||
});
|
||
|
||
refreshButton.addEventListener("mouseleave", () => {
|
||
refreshButton.style.background = "rgba(255, 255, 255, 0.2)";
|
||
});
|
||
|
||
refreshButton.addEventListener("click", () => {
|
||
window.location.reload();
|
||
});
|
||
|
||
notification.appendChild(refreshButton);
|
||
}
|
||
|
||
notification.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||
z-index: 2147483647;
|
||
animation: xgetSlideIn 0.3s ease-out;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
max-width: 280px;
|
||
word-wrap: break-word;
|
||
`;
|
||
|
||
// 如果尚未存在则添加动画样式
|
||
if (!document.getElementById("xget-notification-styles")) {
|
||
const style = document.createElement("style");
|
||
style.id = "xget-notification-styles";
|
||
style.textContent = `
|
||
@keyframes xgetSlideIn {
|
||
from { transform: translateX(100%); opacity: 0; }
|
||
to { transform: translateX(0); opacity: 1; }
|
||
}
|
||
@keyframes xgetSlideOut {
|
||
from { transform: translateX(0); opacity: 1; }
|
||
to { transform: translateX(100%); opacity: 0; }
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
document.body.appendChild(notification);
|
||
|
||
// 如果有刷新按钮则在较长时间后删除,否则在较短时间后删除
|
||
const removeDelay = showRefreshButton ? 8000 : 4000;
|
||
setTimeout(() => {
|
||
notification.style.animation = "xgetSlideOut 0.3s ease-in";
|
||
setTimeout(() => {
|
||
if (notification.parentNode) {
|
||
notification.remove();
|
||
}
|
||
}, 300);
|
||
}, removeDelay);
|
||
} catch (error) {
|
||
console.error("显示通知时出错:", error);
|
||
// 如果 DOM 操作失败则回退到控制台日志
|
||
console.log("Xget 通知:", message);
|
||
}
|
||
}
|