Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0feccdd2b | ||
|
|
1f74a391b7 | ||
|
|
a57fddb857 | ||
|
|
f9abababdb | ||
|
|
92f9326bb9 | ||
|
|
e80768c70a | ||
|
|
be6539c2c2 | ||
|
|
011d269446 | ||
|
|
06a503eab0 | ||
|
|
77e525b64e | ||
|
|
77198bcb62 | ||
|
|
796d50182e | ||
|
|
95bb7d56f9 | ||
|
|
3019487608 | ||
|
|
74759cae4f | ||
|
|
d56ff757ea | ||
|
|
8babb688a6 | ||
|
|
4e8f821de4 | ||
|
|
130eec751f | ||
|
|
ad5b3cae72 | ||
|
|
ed3a4c0ee3 | ||
|
|
367c9fee96 | ||
|
|
d78b231fe9 | ||
|
|
12d2a235bf | ||
|
|
3966bae9c0 | ||
|
|
8b576adbba | ||
|
|
09e87db44a | ||
|
|
d37b67c320 | ||
|
|
9f9a544b00 | ||
|
|
69cf36da68 | ||
|
|
3183c24ae8 | ||
|
|
4dbd0ea25f | ||
|
|
aba18148a7 | ||
|
|
ef4009b913 | ||
|
|
5a210a1d0a | ||
|
|
1f154e475d | ||
|
|
aba0591c3d |
No files matched your search
@@ -12,7 +12,7 @@ body:
|
||||
id: version
|
||||
attributes:
|
||||
label: Extension Version
|
||||
description: What version of Xget for Chromium are you using?
|
||||
description: What version of Xget Now are you using?
|
||||
placeholder: "1.0.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -6,7 +6,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Have a question about Xget for Chromium? We're here to help!
|
||||
Have a question about Xget Now? We're here to help!
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
name: 发布扩展到多个应用商店
|
||||
|
||||
# 工作流触发条件:手动触发,允许指定版本号
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -8,51 +9,77 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
# 工作流权限设置
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
contents: write # 创建 GitHub Release 需要
|
||||
packages: write # 发布包需要
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# 第一步:检出源代码
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# 第二步:设置 Python 环境用于构建脚本
|
||||
- name: 设置 Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
# 第三步:设置 Node.js 环境用于 JSON 验证
|
||||
- name: 设置 Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: 验证 manifest.json
|
||||
# 第四步:验证 manifest 文件格式
|
||||
- name: 验证 manifest 文件
|
||||
run: |
|
||||
# 验证 manifest.json 是否为有效的 JSON
|
||||
# 验证 Chrome manifest.json 是否为有效的 JSON
|
||||
node -e "JSON.parse(require('fs').readFileSync('manifest.json', 'utf8'))"
|
||||
echo "✅ manifest.json 格式有效"
|
||||
echo "✅ Chrome manifest.json 格式有效"
|
||||
|
||||
# 验证 Firefox manifest 是否为有效的 JSON
|
||||
node -e "JSON.parse(require('fs').readFileSync('manifest-firefox.json', 'utf8'))"
|
||||
echo "✅ Firefox manifest.json 格式有效"
|
||||
|
||||
# 第五步:设置版本号
|
||||
- name: 从输入设置版本
|
||||
id: version
|
||||
run: |
|
||||
# 使用手动提供的版本
|
||||
# 从工作流输入获取版本号
|
||||
VERSION="${{ inputs.version }}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "手动设置版本: $VERSION"
|
||||
|
||||
# 第六步:更新所有配置文件中的版本号
|
||||
- name: 更新 manifest 版本
|
||||
run: |
|
||||
# 更新 manifest.json 中的版本以匹配标签
|
||||
# 更新 Chrome manifest.json 中的版本
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
|
||||
manifest.version = '${{ steps.version.outputs.version }}';
|
||||
fs.writeFileSync('manifest.json', JSON.stringify(manifest, null, 2));
|
||||
console.log('已更新 manifest.json 版本为:', manifest.version);
|
||||
console.log('已更新 Chrome manifest.json 版本为:', manifest.version);
|
||||
"
|
||||
|
||||
# 更新 Firefox manifest.json 中的版本
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const manifest = JSON.parse(fs.readFileSync('manifest-firefox.json', 'utf8'));
|
||||
manifest.version = '${{ steps.version.outputs.version }}';
|
||||
fs.writeFileSync('manifest-firefox.json', JSON.stringify(manifest, null, 2));
|
||||
console.log('已更新 Firefox manifest.json 版本为:', manifest.version);
|
||||
"
|
||||
|
||||
# 第七步:同步更新 package.json 版本
|
||||
- name: 更新 package.json 版本
|
||||
run: |
|
||||
# 更新 package.json 中的版本以匹配标签
|
||||
# 更新 package.json 中的版本以保持一致性
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
@@ -61,14 +88,15 @@ jobs:
|
||||
console.log('已更新 package.json 版本为:', pkg.version);
|
||||
"
|
||||
|
||||
# 第八步:提交版本更改并创建 Git 标签
|
||||
- name: 提交并推送版本更新
|
||||
run: |
|
||||
# 配置 git 用户信息以进行提交
|
||||
# 配置 Git 用户信息用于自动提交
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
# 添加更新的文件
|
||||
git add manifest.json package.json
|
||||
git add manifest.json manifest-firefox.json package.json
|
||||
|
||||
# 检查是否有需要提交的更改
|
||||
if git diff --staged --quiet; then
|
||||
@@ -87,75 +115,35 @@ jobs:
|
||||
echo "✅ 版本更新已提交并推送到主分支,包含标签"
|
||||
fi
|
||||
|
||||
- name: 创建扩展包
|
||||
- name: 构建扩展包
|
||||
run: |
|
||||
# 创建 Chrome 扩展 zip 包
|
||||
zip -r chrome-extension.zip . \
|
||||
-x "*.git*" \
|
||||
-x "node_modules/*" \
|
||||
-x ".github/*" \
|
||||
-x "*.md" \
|
||||
-x "LICENSE" \
|
||||
-x "*.zip" \
|
||||
-x "*.crx"
|
||||
echo "🏗️ 开始构建扩展包..."
|
||||
|
||||
# 创建 Microsoft Edge 扩展 zip 包(与 Chrome 的 MV3 相同)
|
||||
cp chrome-extension.zip edge-extension.zip
|
||||
# 运行构建脚本构建所有平台
|
||||
python build.py --platform all --package
|
||||
|
||||
echo "📦 已创建扩展包"
|
||||
ls -la *.zip
|
||||
echo "📦 扩展包构建完成"
|
||||
ls -la packages/ || echo "packages 目录不存在"
|
||||
|
||||
- name: 为 GitHub 发布创建 CRX 文件
|
||||
- name: 运行测试
|
||||
run: |
|
||||
# 安装 chrome-extension-packer
|
||||
npm install -g crx3
|
||||
echo "🧪 运行基础测试..."
|
||||
python dev.py test
|
||||
|
||||
# 为 CRX 签名创建私钥(仅用于分发)
|
||||
openssl genrsa -out key.pem 2048
|
||||
echo "🔍 运行代码检查..."
|
||||
python dev.py lint
|
||||
|
||||
# 验证 manifest.json 是否存在
|
||||
ls -la manifest.json
|
||||
echo "当前目录: $(pwd)"
|
||||
echo "目录内容:"
|
||||
ls -la
|
||||
|
||||
# 为扩展文件创建临时目录
|
||||
mkdir -p extension-temp
|
||||
|
||||
# 将扩展文件复制到临时目录
|
||||
cp manifest.json extension-temp/
|
||||
cp background.js extension-temp/
|
||||
cp content.js extension-temp/
|
||||
cp popup.html extension-temp/
|
||||
cp popup.js extension-temp/
|
||||
cp -r icons extension-temp/
|
||||
|
||||
# 验证临时目录内容
|
||||
echo "扩展临时目录内容:"
|
||||
ls -la extension-temp/
|
||||
|
||||
# 从临时目录创建 CRX 文件
|
||||
cd extension-temp
|
||||
crx3 -o ../extension.crx -p ../key.pem .
|
||||
cd ..
|
||||
|
||||
echo "📦 已创建 extension.crx"
|
||||
ls -la extension.crx
|
||||
|
||||
# 清理
|
||||
rm -rf extension-temp
|
||||
|
||||
- name: 发布到 Chrome 网上应用店
|
||||
- name: 发布到 Chrome 应用商店
|
||||
continue-on-error: true
|
||||
id: chrome-publish
|
||||
uses: mnao305/chrome-extension-upload@v5.0.0
|
||||
with:
|
||||
file-path: chrome-extension.zip
|
||||
file-path: packages/Xget-Now_${{ steps.version.outputs.version }}.chromium.zip
|
||||
extension-id: ${{ secrets.CHROME_EXTENSION_ID }}
|
||||
client-id: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
publish: true # 如果只想上传而不发布,设置为 false
|
||||
publish: true
|
||||
|
||||
- name: 发布到 Microsoft Edge 加载项
|
||||
continue-on-error: true
|
||||
@@ -163,35 +151,55 @@ jobs:
|
||||
uses: wdzeng/edge-addon@v2
|
||||
with:
|
||||
product-id: ${{ secrets.EDGE_PRODUCT_ID }}
|
||||
zip-path: edge-extension.zip
|
||||
zip-path: packages/Xget-Now_${{ steps.version.outputs.version }}.chromium.zip
|
||||
api-key: ${{ secrets.EDGE_API_KEY }}
|
||||
client-id: ${{ secrets.EDGE_CLIENT_ID }}
|
||||
notes-for-certification: "自动发布 v${{ steps.version.outputs.version }} - ${{ github.sha }}"
|
||||
|
||||
- name: 发布到 Firefox 附加组件
|
||||
continue-on-error: true
|
||||
id: firefox-publish
|
||||
uses: browser-actions/release-firefox-addon@latest
|
||||
with:
|
||||
addon-id: ${{ secrets.FIREFOX_ADDON_ID }}
|
||||
addon-path: packages/Xget-Now_${{ steps.version.outputs.version }}.firefox.xpi
|
||||
auth-api-issuer: ${{ secrets.FIREFOX_AUTH_API_ISSUER }}
|
||||
auth-api-secret: ${{ secrets.FIREFOX_AUTH_API_SECRET }}
|
||||
approval-note: |
|
||||
这是一个下载加速器扩展,支持多个平台的文件下载加速。
|
||||
源代码位于 GitHub: https://github.com/xixu-me/Xget-Now
|
||||
构建说明:直接从源代码打包,无需编译或转译。
|
||||
|
||||
- name: 创建 GitHub 发布
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.version.outputs.version }}
|
||||
name: Xget for Chromium ${{ steps.version.outputs.version }}
|
||||
name: Xget Now ${{ steps.version.outputs.version }}
|
||||
files: |
|
||||
chrome-extension.zip
|
||||
edge-extension.zip
|
||||
extension.crx
|
||||
packages/Xget-Now_${{ steps.version.outputs.version }}.chromium.zip
|
||||
packages/Xget-Now_${{ steps.version.outputs.version }}.firefox.xpi
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
此版本已自动发布到多个扩展商店。
|
||||
## 📦 扩展包下载
|
||||
|
||||
## 📦 安装选项
|
||||
### Chromium
|
||||
- **文件**: `Xget-Now_${{ steps.version.outputs.version }}.chromium.zip`
|
||||
- **支持**: Chrome、Microsoft Edge 等基于 Chromium 的浏览器
|
||||
- **Manifest**: v3
|
||||
|
||||
- **Chrome 网上应用店**: [从 Chrome 网上应用店安装](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf)
|
||||
- **Microsoft Edge 加载项**: [从 Microsoft Edge 加载项安装](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc)
|
||||
- **手动安装**: 下载下面的文件或查看 [安装指南](https://github.com/xixu-me/Xget-for-Chromium?tab=readme-ov-file#-%E5%AE%89%E8%A3%85) 获取详细说明
|
||||
### Firefox
|
||||
- **文件**: `Xget-Now_${{ steps.version.outputs.version }}.firefox.xpi`
|
||||
- **支持**: Firefox 或其他基于 Firefox 的浏览器
|
||||
- **Manifest**: v2
|
||||
|
||||
## 📁 可用文件
|
||||
## 🚀 安装方法
|
||||
|
||||
详细的安装说明请参考:[手动安装指南](https://github.com/xixu-me/Xget-Now?tab=readme-ov-file#%E6%89%8B%E5%8A%A8%E5%AE%89%E8%A3%85)
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
详细的更改内容请查看下方的自动生成的发布说明。
|
||||
|
||||
- `chrome-extension.zip` - 适用于 Chrome 浏览器
|
||||
- `edge-extension.zip` - 适用于 Microsoft Edge
|
||||
- `extension.crx` - 适用于所有基于 Chromium 的浏览器的预打包扩展文件
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -199,13 +207,15 @@ jobs:
|
||||
run: |
|
||||
echo "📊 发布结果摘要:"
|
||||
echo "================================"
|
||||
echo "📦 版本: ${{ steps.version.outputs.version }}"
|
||||
echo ""
|
||||
|
||||
if [ "${{ steps.chrome-publish.outcome }}" = "success" ]; then
|
||||
echo "✅ Chrome 网上应用店: 发布成功"
|
||||
echo "✅ Chrome 应用商店: 发布成功"
|
||||
elif [ "${{ steps.chrome-publish.outcome }}" = "failure" ]; then
|
||||
echo "❌ Chrome 网上应用店: 发布失败"
|
||||
echo "❌ Chrome 应用商店: 发布失败"
|
||||
else
|
||||
echo "⚠️ Chrome 网上应用店: 已跳过(缺少密钥)"
|
||||
echo "⚠️ Chrome 应用商店: 已跳过(缺少密钥或其他原因)"
|
||||
fi
|
||||
|
||||
if [ "${{ steps.edge-publish.outcome }}" = "success" ]; then
|
||||
@@ -213,15 +223,33 @@ jobs:
|
||||
elif [ "${{ steps.edge-publish.outcome }}" = "failure" ]; then
|
||||
echo "❌ Microsoft Edge 加载项: 发布失败"
|
||||
else
|
||||
echo "⚠️ Microsoft Edge 加载项: 已跳过(缺少密钥)"
|
||||
echo "⚠️ Microsoft Edge 加载项: 已跳过(缺少密钥或其他原因)"
|
||||
fi
|
||||
|
||||
echo "✅ GitHub 发布: 已创建扩展文件"
|
||||
if [ "${{ steps.firefox-publish.outcome }}" = "success" ]; then
|
||||
echo "✅ Firefox 附加组件: 发布成功"
|
||||
elif [ "${{ steps.firefox-publish.outcome }}" = "failure" ]; then
|
||||
echo "❌ Firefox 附加组件: 发布失败"
|
||||
else
|
||||
echo "⚠️ Firefox 附加组件: 已跳过(缺少密钥或其他原因)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ GitHub 发布: 已创建包含所有扩展文件"
|
||||
echo "================================"
|
||||
|
||||
- name: 通知成功
|
||||
run: |
|
||||
echo "🎉 成功发布 Xget for Chromium ${{ steps.version.outputs.version }} 到多个商店!"
|
||||
echo "🔗 Chrome 网上应用店: https://chrome.google.com/webstore/detail/${{ secrets.CHROME_EXTENSION_ID }}"
|
||||
echo "🔗 Microsoft Edge 加载项: 查看发布页面获取手动安装文件"
|
||||
echo ""
|
||||
echo "🎉 成功发布 Xget Now ${{ steps.version.outputs.version }}!"
|
||||
echo "================================"
|
||||
echo "🔗 Chrome 应用商店: https://chrome.google.com/webstore/detail/${{ secrets.CHROME_EXTENSION_ID }}"
|
||||
echo "🔗 Microsoft Edge 加载项: 查看发布页面获取最新信息"
|
||||
echo "🦊 Firefox 附加组件: 查看 Mozilla Add-ons 获取最新信息"
|
||||
echo "📦 GitHub 发布: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}"
|
||||
echo "📖 安装指南: https://github.com/${{ github.repository }}#installation"
|
||||
echo ""
|
||||
echo "📁 发布的文件:"
|
||||
echo " - Xget-Now_${{ steps.version.outputs.version }}.chromium.zip (Chromium)"
|
||||
echo " - Xget-Now_${{ steps.version.outputs.version }}.firefox.xpi (Firefox)"
|
||||
echo "================================"
|
||||
@@ -0,0 +1,198 @@
|
||||
name: 测试构建
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: 设置 Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: 设置 Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: 验证 manifest 文件
|
||||
run: |
|
||||
echo "验证 Chrome manifest..."
|
||||
node -e "
|
||||
const manifest = JSON.parse(require('fs').readFileSync('manifest.json', 'utf8'));
|
||||
console.log('Chrome manifest 版本:', manifest.version);
|
||||
console.log('Manifest 版本:', manifest.manifest_version);
|
||||
if (manifest.manifest_version !== 3) {
|
||||
throw new Error('Chrome manifest 应该使用 Manifest V3');
|
||||
}
|
||||
"
|
||||
|
||||
echo "验证 Firefox manifest..."
|
||||
node -e "
|
||||
const manifest = JSON.parse(require('fs').readFileSync('manifest-firefox.json', 'utf8'));
|
||||
console.log('Firefox manifest 版本:', manifest.version);
|
||||
console.log('Manifest 版本:', manifest.manifest_version);
|
||||
if (manifest.manifest_version !== 2) {
|
||||
throw new Error('Firefox manifest 应该使用 Manifest V2');
|
||||
}
|
||||
"
|
||||
|
||||
- name: 检查必需文件
|
||||
run: |
|
||||
echo "检查必需文件是否存在..."
|
||||
|
||||
required_files=(
|
||||
"manifest.json"
|
||||
"manifest-firefox.json"
|
||||
"background.js"
|
||||
"content.js"
|
||||
"popup.js"
|
||||
"popup.html"
|
||||
"webext-compat.js"
|
||||
"build.py"
|
||||
"dev.py"
|
||||
)
|
||||
|
||||
for file in "${required_files[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "✅ $file"
|
||||
else
|
||||
echo "❌ $file 缺失"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: 运行基础测试
|
||||
run: |
|
||||
echo "运行基础测试..."
|
||||
python dev.py test
|
||||
|
||||
- name: 运行代码检查
|
||||
run: |
|
||||
echo "运行代码检查..."
|
||||
python dev.py lint
|
||||
|
||||
- name: 测试 Chrome 构建
|
||||
run: |
|
||||
echo "测试 Chrome 构建..."
|
||||
python build.py --platform chrome
|
||||
|
||||
# 验证构建结果
|
||||
if [ -d "build/chrome" ]; then
|
||||
echo "✅ Chrome 构建目录已创建"
|
||||
ls -la build/chrome/
|
||||
else
|
||||
echo "❌ Chrome 构建失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 测试 Firefox 构建
|
||||
run: |
|
||||
echo "测试 Firefox 构建..."
|
||||
python build.py --platform firefox
|
||||
|
||||
# 验证构建结果
|
||||
if [ -d "build/firefox" ]; then
|
||||
echo "✅ Firefox 构建目录已创建"
|
||||
ls -la build/firefox/
|
||||
else
|
||||
echo "❌ Firefox 构建失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 测试扩展包生成
|
||||
run: |
|
||||
echo "测试扩展包生成..."
|
||||
python build.py --platform all --package
|
||||
|
||||
# 验证扩展包
|
||||
if [ -d "packages" ]; then
|
||||
echo "✅ 扩展包目录已创建"
|
||||
ls -la packages/
|
||||
|
||||
# 检查是否有 Chrome 和 Firefox 包
|
||||
if [ -f packages/xget-now-chrome-v*.zip ]; then
|
||||
echo "✅ Chrome 扩展包已生成"
|
||||
else
|
||||
echo "❌ Chrome 扩展包生成失败"
|
||||
fi
|
||||
|
||||
if [ -f packages/xget-now-firefox-v*.zip ]; then
|
||||
echo "✅ Firefox 扩展包已生成"
|
||||
else
|
||||
echo "❌ Firefox 扩展包生成失败"
|
||||
fi
|
||||
else
|
||||
echo "❌ 扩展包生成失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 验证构建的 manifest 文件
|
||||
run: |
|
||||
echo "验证构建的 manifest 文件..."
|
||||
|
||||
# 检查 Chrome 构建的 manifest
|
||||
echo "检查 Chrome manifest..."
|
||||
node -e "
|
||||
const manifest = JSON.parse(require('fs').readFileSync('build/chrome/manifest.json', 'utf8'));
|
||||
console.log('Chrome 构建版本:', manifest.version);
|
||||
if (manifest.manifest_version !== 3) {
|
||||
throw new Error('Chrome 构建应该使用 Manifest V3');
|
||||
}
|
||||
if (!manifest.action) {
|
||||
throw new Error('Chrome 构建应该有 action 字段');
|
||||
}
|
||||
"
|
||||
|
||||
# 检查 Firefox 构建的 manifest
|
||||
echo "检查 Firefox manifest..."
|
||||
node -e "
|
||||
const manifest = JSON.parse(require('fs').readFileSync('build/firefox/manifest.json', 'utf8'));
|
||||
console.log('Firefox 构建版本:', manifest.version);
|
||||
if (manifest.manifest_version !== 2) {
|
||||
throw new Error('Firefox 构建应该使用 Manifest V2');
|
||||
}
|
||||
if (!manifest.browser_action) {
|
||||
throw new Error('Firefox 构建应该有 browser_action 字段');
|
||||
}
|
||||
if (!manifest.applications && !manifest.browser_specific_settings) {
|
||||
throw new Error('Firefox 构建应该有 applications 或 browser_specific_settings 字段');
|
||||
}
|
||||
"
|
||||
|
||||
- name: 上传构建结果
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: extension-builds-${{ github.sha }}
|
||||
path: |
|
||||
build/
|
||||
packages/
|
||||
retention-days: 7
|
||||
|
||||
- name: 测试总结
|
||||
run: |
|
||||
echo ""
|
||||
echo "🎉 所有测试通过!"
|
||||
echo "================================"
|
||||
echo "✅ Manifest 文件验证"
|
||||
echo "✅ 必需文件检查"
|
||||
echo "✅ 基础测试"
|
||||
echo "✅ 代码检查"
|
||||
echo "✅ Chrome 构建"
|
||||
echo "✅ Firefox 构建"
|
||||
echo "✅ 扩展包生成"
|
||||
echo "✅ 构建结果验证"
|
||||
echo "================================"
|
||||
echo "🚀 准备发布!"
|
||||
+15
-13
@@ -1,16 +1,16 @@
|
||||
# 隐私政策
|
||||
|
||||
## 生效日期:2025 年 7 月 12 日
|
||||
## 生效日期:2025 年 8 月 10 日
|
||||
|
||||
## 概述
|
||||
|
||||
Xget for Chromium 是一个开源浏览器扩展,旨在通过 Xget 代理服务重定向下载链接来加速 GitHub、GitLab 和 Hugging Face 的下载。我们承诺保护您的隐私并确保数据处理的透明度。
|
||||
Xget Now 是一个开源浏览器扩展,旨在通过 Xget 代理服务重定向下载链接来加速众多软件开发和研究平台的下载,包括 GitHub、GitLab、Hugging Face、npm、PyPI、Maven、RubyGems、CRAN 等 20+ 个平台。我们承诺保护您的隐私并确保数据处理的透明度。
|
||||
|
||||
## 我们的隐私承诺
|
||||
|
||||
**我们不收集、存储或传输您的个人数据。**
|
||||
|
||||
Xget for Chromium 扩展遵循“隐私优先”的设计原则,所有处理都在您的浏览器本地进行。
|
||||
Xget Now 扩展遵循“隐私优先”的设计原则,所有处理都在您的浏览器本地进行。
|
||||
|
||||
## 收集的信息
|
||||
|
||||
@@ -35,7 +35,7 @@ Xget for Chromium 扩展遵循“隐私优先”的设计原则,所有处理
|
||||
|
||||
- **Xget 域名配置**:您设置的自定义 Xget 服务域名
|
||||
- **扩展启用状态**:扩展是否启用的开关状态
|
||||
- **平台偏好设置**:为 GitHub、GitLab 和 Hugging Face 启用/禁用的选择
|
||||
- **平台偏好设置**:为支持的各个平台(GitHub、GitLab、Hugging Face、npm、PyPI、Maven、RubyGems、CRAN 等)启用/禁用的选择
|
||||
|
||||
这些设置:
|
||||
|
||||
@@ -84,9 +84,11 @@ Xget for Chromium 扩展遵循“隐私优先”的设计原则,所有处理
|
||||
|
||||
该扩展在以下平台上运行:
|
||||
|
||||
- GitHub (github.com)
|
||||
- GitLab (gitlab.com)
|
||||
- Hugging Face (huggingface.co)
|
||||
- **代码托管平台**:GitHub、GitLab、Gitea、Codeberg、SourceForge、Android Source
|
||||
- **包管理平台**:npm、PyPI、Anaconda、Maven Central、Gradle Plugin Portal、RubyGems、CRAN、CPAN、CTAN、Go Proxy、NuGet、Crates.io、Packagist
|
||||
- **AI/ML 平台**:Hugging Face
|
||||
- **学术平台**:arXiv
|
||||
- **应用分发**:F-Droid
|
||||
|
||||
扩展不会与这些平台共享任何数据,也不会修改您与这些平台的交互,除了重定向下载链接。
|
||||
|
||||
@@ -99,14 +101,14 @@ Xget for Chromium 扩展遵循“隐私优先”的设计原则,所有处理
|
||||
1. **`downloads`**:检测和管理下载以进行重定向
|
||||
2. **`storage`**:在本地存储您的扩展设置
|
||||
3. **`activeTab`**:在当前标签页中显示通知
|
||||
4. **`host_permissions`**:访问受支持平台以检测下载链接
|
||||
4. **`host_permissions`**:访问受支持的 20+ 个平台以检测下载链接
|
||||
|
||||
### 权限使用
|
||||
|
||||
- **下载权限**:仅用于检测下载并将其重定向到 Xget
|
||||
- **存储权限**:仅用于保存您的扩展设置
|
||||
- **标签页权限**:仅用于显示下载重定向通知
|
||||
- **主机权限**:仅用于在受支持平台上检测下载链接
|
||||
- **主机权限**:仅用于在受支持的各个平台上检测下载链接
|
||||
|
||||
我们不使用这些权限来收集数据、跟踪您的活动或任何其他目的。
|
||||
|
||||
@@ -121,7 +123,7 @@ Xget for Chromium 扩展遵循“隐私优先”的设计原则,所有处理
|
||||
### 开源透明性
|
||||
|
||||
- 扩展是完全开源的
|
||||
- 源代码可在 [GitHub](https://github.com/xixu-me/Xget-for-Chromium) 上获得
|
||||
- 源代码可在 [GitHub](https://github.com/xixu-me/Xget-Now) 上获得
|
||||
- 您可以审查、验证和审计所有代码
|
||||
|
||||
## 您的权利
|
||||
@@ -168,7 +170,7 @@ Xget for Chromium 扩展遵循“隐私优先”的设计原则,所有处理
|
||||
|
||||
如果您对此隐私政策有任何问题、疑虑或建议,请:
|
||||
|
||||
- **GitHub Issues**:在我们的 [GitHub 存储库](https://github.com/xixu-me/Xget-for-Chromium/issues) 上打开一个 issue
|
||||
- **GitHub Issues**:在我们的 [GitHub 存储库](https://github.com/xixu-me/Xget-Now/issues) 上打开一个 issue
|
||||
- **电子邮件**:通过我们的 GitHub 配置文件联系维护者
|
||||
|
||||
## 法律合规
|
||||
@@ -210,7 +212,7 @@ Xget for Chromium 扩展遵循“隐私优先”的设计原则,所有处理
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2025 年 7 月 18 日
|
||||
**联系方式**:[GitHub Issues](https://github.com/xixu-me/Xget-for-Chromium/issues)
|
||||
**最后更新**:2025 年 8 月 10 日
|
||||
**联系方式**:[GitHub Issues](https://github.com/xixu-me/Xget-Now/issues)
|
||||
|
||||
此隐私政策是我们对透明度和用户隐私承诺的一部分。我们相信您有权了解您使用的软件如何处理您的数据,以及在这种情况下,确认它不会处理您的个人数据。
|
||||
@@ -1,24 +1,23 @@
|
||||
# Xget for Chromium
|
||||
# Xget Now
|
||||
|
||||
一个强大的 Chromium 浏览器扩展,通过自动将下载重定向到 [Xget](https://github.com/xixu-me/Xget) 代理服务,从而加速 GitHub、GitLab 和 Hugging Face 的下载。
|
||||
为 Chromium 和 Firefox 浏览器提供的 [Xget](https://github.com/xixu-me/Xget) 下载加速扩展,支持 GitHub、GitLab、npm、PyPI 等众多平台的文件下载加速。
|
||||
|
||||
## 🚀 功能特性
|
||||
|
||||
- **🎯 自动下载加速**:无缝重定向下载通过 Xget 获得更快速度
|
||||
- **🌐 多平台支持**:支持 GitHub、GitLab 和 Hugging Face
|
||||
- **⚙️ 可配置设置**:自定义你的 Xget 域名和平台偏好
|
||||
- **🔔 智能通知**:下载重定向时的可视化反馈
|
||||
- **🛡️ 隐私优先**:所有处理都在你的浏览器本地进行
|
||||
- **🎛️ 按平台控制**:为特定平台启用/禁用加速
|
||||
- **🌐 跨浏览器支持**:同时支持 Chromium 和 Firefox 浏览器
|
||||
|
||||
## 📦 安装
|
||||
|
||||
### 应用商店可用性
|
||||
### 商店可用性
|
||||
|
||||
| 商店 | 链接 |
|
||||
|-------|------|
|
||||
| **Chrome 网上应用店** | [](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf) |
|
||||
| **Microsoft Edge 加载项** | [Available in the Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc) |
|
||||
- [Chrome 应用商店](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf?hl=zh-CN)
|
||||
- [Firefox 附加组件](https://addons.mozilla.org/zh-CN/firefox/addon/xget-now/)
|
||||
- [Microsoft Edge 加载项](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc?hl=zh-CN&gl=CN)
|
||||
|
||||
### 手动安装
|
||||
|
||||
@@ -27,41 +26,73 @@
|
||||
#### 选项 1:从 GitHub Releases 安装(推荐)
|
||||
|
||||
1. **下载扩展**
|
||||
- 前往 [Releases 页面](https://github.com/xixu-me/Xget-for-Chromium/releases)
|
||||
- 下载最新发布文件:
|
||||
- `chrome-extension.zip` - 适用于 Chrome 浏览器
|
||||
- `edge-extension.zip` - 适用于 Microsoft Edge
|
||||
- `extension.crx` - 适用于所有基于 Chromium 的浏览器的预打包扩展文件
|
||||
- 前往 [Releases 页面](https://github.com/xixu-me/Xget-Now/releases/latest)
|
||||
- 下载对应浏览器的扩展文件:
|
||||
- `Xget-Now_x.x.x.chromium.zip` - 适用于所有基于 Chromium 的浏览器(Chrome、Microsoft Edge、Opera 等)
|
||||
- `Xget-Now_x.x.x.firefox.zip` - 适用于 Firefox 浏览器
|
||||
|
||||
2. **在 Chrome 中安装**
|
||||
- 解压下载的 Chromium 版本 ZIP 文件
|
||||
- 打开 Chrome 并前往 `chrome://extensions/`
|
||||
- 启用「开发者模式」(右上角的切换开关)
|
||||
- 点击「加载已解压的扩展程序」并选择从 ZIP 文件解压的文件夹
|
||||
- 或者直接将 `.crx` 文件拖放到扩展页面
|
||||
- 点击「加载已解压的扩展程序」并选择解压的文件夹
|
||||
|
||||
3. **在 Microsoft Edge 中安装**
|
||||
3. **在 Firefox 中安装**
|
||||
- 打开 Firefox 并前往 `about:debugging`
|
||||
- 点击「此 Firefox」
|
||||
- 点击「临时载入附加组件」
|
||||
- 选择下载的 Firefox 版本 ZIP 文件或解压后的 `manifest.json` 文件
|
||||
|
||||
4. **在 Microsoft Edge 中安装**
|
||||
- 解压下载的 Chromium 版本 ZIP 文件
|
||||
- 打开 Microsoft Edge 并前往 `edge://extensions/`
|
||||
- 启用「开发人员模式」(左侧边栏的切换开关)
|
||||
- 点击「加载解压缩的扩展」并选择从 ZIP 文件解压的文件夹
|
||||
- 或者直接将 `.crx` 文件拖放到扩展页面
|
||||
- 点击「加载解压缩的扩展」并选择解压的文件夹
|
||||
|
||||
4. **在其他基于 Chromium 的浏览器中安装**
|
||||
5. **在其他基于 Chromium 的浏览器中安装**
|
||||
- 使用 Chromium 版本的扩展包
|
||||
- 遵循与 Chrome 或 Microsoft Edge 类似的步骤,确保首先启用「开发者模式」
|
||||
- 直接将 `.crx` 文件拖放到扩展页面
|
||||
- 按照相应浏览器的扩展安装指南进行操作
|
||||
|
||||
#### 选项 2:从源码安装
|
||||
|
||||
1. **克隆存储库**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/xixu-me/Xget-for-Chromium.git
|
||||
cd Xget-for-Chromium
|
||||
git clone https://github.com/xixu-me/Xget-Now.git
|
||||
cd Xget-Now
|
||||
```
|
||||
|
||||
2. **在浏览器中加载**
|
||||
- 打开浏览器的扩展管理页面
|
||||
2. **构建扩展(可选)**
|
||||
|
||||
如果你想使用优化过的版本:
|
||||
|
||||
```bash
|
||||
# 安装 Python 3.7+
|
||||
pip install -r requirements.txt # 如果有依赖文件
|
||||
|
||||
# 构建特定浏览器版本
|
||||
python build.py --platform chrome # 构建 Chrome 版本
|
||||
python build.py --platform firefox # 构建 Firefox 版本
|
||||
python build.py --platform all # 构建所有版本
|
||||
```
|
||||
|
||||
3. **在浏览器中加载**
|
||||
|
||||
**Chromium 浏览器:**
|
||||
- 打开浏览器的扩展管理页面(`chrome://extensions/` 或 `edge://extensions/`)
|
||||
- 启用「开发者模式」
|
||||
- 点击「加载已解压的扩展程序」并选择克隆的文件夹
|
||||
- 点击「加载已解压的扩展程序」并选择:
|
||||
- 源码根目录(如果使用原始源码)
|
||||
- `build/chrome/` 目录(如果使用构建版本)
|
||||
|
||||
**Firefox 浏览器:**
|
||||
- 打开 `about:debugging`
|
||||
- 点击「此 Firefox」
|
||||
- 点击「临时载入附加组件」
|
||||
- 选择以下文件之一:
|
||||
- 源码根目录中的 `manifest-firefox.json`
|
||||
- `build/firefox/` 目录中的 `manifest.json`(如果使用构建版本)
|
||||
|
||||
#### 验证安装
|
||||
|
||||
@@ -74,29 +105,15 @@
|
||||
- 输入你的 Xget 域名(例如 `xget.xi-xu.me`)
|
||||
- 启用扩展
|
||||
|
||||
2. **选择平台**(可选)
|
||||
2. **选择平台**
|
||||
- 根据需要切换各个平台的开关
|
||||
- 默认启用所有平台(GitHub、GitLab、Hugging Face)
|
||||
- 默认启用所有平台
|
||||
|
||||
3. **开始下载**
|
||||
- 访问任何受支持的平台
|
||||
- 像往常一样点击下载链接
|
||||
- 下载将自动通过 Xget 加速
|
||||
|
||||
## 🎯 支持的平台
|
||||
|
||||
| 平台 | 状态 | URL 模式 | 扩展 ID |
|
||||
|----------|---------|-------------|--------------|
|
||||
| **GitHub** | ✅ 支持 | `github.com/*` | `gh` |
|
||||
| **GitLab** | ✅ 支持 | `gitlab.com/*` | `gl` |
|
||||
| **Hugging Face** | ✅ 支持 | `huggingface.co/*` | `hf` |
|
||||
|
||||
### 支持的下载类型
|
||||
|
||||
- **GitHub**:发布资源、存储库存档、原始文件、Git LFS 文件
|
||||
- **GitLab**:项目存档、发布下载、存储库导出
|
||||
- **Hugging Face**:模型文件、数据集文件、模型卡片、分词器
|
||||
|
||||
## 🔧 工作原理
|
||||
|
||||
1. **检测**:扩展监控受支持平台上的下载链接
|
||||
@@ -104,19 +121,6 @@
|
||||
3. **重定向**:下载通过你配置的 Xget 域名路由
|
||||
4. **加速**:Xget 的全球 CDN 和优化功能提供更快的下载速度
|
||||
|
||||
### URL 转换示例
|
||||
|
||||
```text
|
||||
原始: https://github.com/user/repo/archive/main.zip
|
||||
Xget: https://xget.xi-xu.me/gh/user/repo/archive/main.zip
|
||||
|
||||
原始: https://gitlab.com/user/repo/-/archive/main.zip
|
||||
Xget: https://xget.xi-xu.me/gl/user/repo/-/archive/main.zip
|
||||
|
||||
原始: https://huggingface.co/user/model/resolve/main/model.bin
|
||||
Xget: https://xget.xi-xu.me/hf/user/model/resolve/main/model.bin
|
||||
```
|
||||
|
||||
### 为什么 Xget 让下载更快
|
||||
|
||||
- **边缘缓存**:文件在 Cloudflare 的全球边缘位置进行缓存
|
||||
@@ -135,29 +139,33 @@ Xget: https://xget.xi-xu.me/hf/user/model/resolve/main/model.bin
|
||||
- **不稳定连接**:自动重试机制处理网络问题
|
||||
- **并发下载**:与直接平台下载相比,没有速率限制
|
||||
|
||||
### 基准测试
|
||||
|
||||
| 文件类型 | 原始速度 | 使用 Xget | 提升 |
|
||||
|-----------|----------------|-----------|-------------|
|
||||
| GitHub Release (100MB) | 2.5 MB/s | 12.8 MB/s | **快 5.1 倍** |
|
||||
| 存储库存档 (50MB) | 1.8 MB/s | 8.2 MB/s | **快 4.6 倍** |
|
||||
| Hugging Face 模型 (500MB) | 3.2 MB/s | 15.1 MB/s | **快 4.7 倍** |
|
||||
|
||||
> **注意**:结果可能因位置、网络条件和服务器负载而异
|
||||
|
||||
## 📋 要求
|
||||
|
||||
- **Chrome 浏览器**:版本 88+(Manifest V3 支持)
|
||||
### 浏览器支持
|
||||
|
||||
**Chromium 浏览器:**
|
||||
|
||||
- **Chrome**:版本 88+(Manifest V3 支持)
|
||||
- **Microsoft Edge**:版本 88+
|
||||
- **Opera**:版本 74+
|
||||
- **其他基于 Chromium 的浏览器**:支持 Manifest V3 的版本
|
||||
|
||||
**Firefox 浏览器:**
|
||||
|
||||
- **Firefox**:版本 109+(Manifest V2 支持)
|
||||
- **Firefox ESR**:版本 109+
|
||||
|
||||
### Xget 服务要求
|
||||
|
||||
- **Xget 服务**:访问 Xget 实例
|
||||
- 使用公共实例:`xget.xi-xu.me`
|
||||
- 或部署你自己的:[Xget 存储库](https://github.com/xixu-me/Xget)(推荐)
|
||||
- 或部署你自己的:[Xget 存储库](https://github.com/xixu-me/Xget)
|
||||
|
||||
### 关于 Xget 服务
|
||||
|
||||
Xget 是基于 Cloudflare Workers 构建的高性能、安全的代理服务,提供:
|
||||
|
||||
- **全球边缘分发**:通过 Cloudflare 的全球网络加快下载速度
|
||||
- **多平台支持**:针对 GitHub、GitLab 和 Hugging Face 进行优化
|
||||
- **高级功能**:HTTP/3 支持、智能缓存、自动重试
|
||||
- **安全性**:全面的安全标头和内容保护
|
||||
- **可靠性**:内置超时保护和性能监控
|
||||
@@ -205,17 +213,35 @@ Xget 是基于 Cloudflare Workers 构建的高性能、安全的代理服务,
|
||||
- 验证目标平台的服务器是否响应
|
||||
- 清除浏览器缓存并重新加载扩展
|
||||
|
||||
**Firefox 特定问题?**
|
||||
|
||||
- 确保 Firefox 版本为 109+
|
||||
- 在 `about:debugging` 中检查扩展是否正确加载
|
||||
- Firefox 可能需要重启浏览器才能完全应用扩展设置
|
||||
- 检查 Firefox 的「隐私和安全」设置是否阻止了扩展功能
|
||||
|
||||
### 调试模式
|
||||
|
||||
**Chromium 浏览器:**
|
||||
|
||||
启用 Chrome 开发者工具并检查控制台选项卡中的调试消息:
|
||||
|
||||
- 扩展加载:「Xget for Chromium:内容脚本已加载」
|
||||
- 下载重定向:「重定向下载:[原始] -> [xget]」
|
||||
**Firefox 浏览器:**
|
||||
|
||||
1. 打开 `about:debugging`
|
||||
2. 点击「此 Firefox」
|
||||
3. 找到 Xget Now 扩展并点击「检查」
|
||||
4. 在打开的开发者工具中查看控制台消息
|
||||
|
||||
**通用调试消息:**
|
||||
|
||||
- 扩展加载:「Xget Now:内容脚本已加载」
|
||||
- 下载重定向:「重定向下载:[原始] -> [Xget]」
|
||||
- 设置更改:「设置已更新!点击刷新页面」
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 GPL-3.0 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
本存储库采用 GPL-3.0 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
@@ -223,10 +249,6 @@ Xget 是基于 Cloudflare Workers 构建的高性能、安全的代理服务,
|
||||
|
||||
是的,扩展完全免费且在 GPL-3.0 许可证下开源。
|
||||
|
||||
### 这对私有存储库有效吗?
|
||||
|
||||
扩展适用于任何可下载链接,但你需要对私有存储库有适当的访问权限。
|
||||
|
||||
### 我可以使用自己的 Xget 服务器吗?
|
||||
|
||||
当然可以!你可以使用 [Xget 存储库](https://github.com/xixu-me/Xget) 部署自己的 Xget 实例,并配置扩展使用你的域名。
|
||||
@@ -247,11 +269,27 @@ Xget 是基于 Cloudflare Workers 构建的高性能、安全的代理服务,
|
||||
|
||||
是的,使用扩展弹出窗口中的按平台切换开关来禁用特定平台的加速。
|
||||
|
||||
### 为什么 Chrome 和 Firefox 版本不同?
|
||||
|
||||
由于浏览器架构差异,我们针对不同浏览器进行了优化:
|
||||
|
||||
- **Chrome 版本**:使用 Manifest V3,支持最新的扩展 API 和安全特性
|
||||
- **Firefox 版本**:使用 Manifest V2,确保与 Firefox 扩展系统的最佳兼容性
|
||||
|
||||
两个版本的核心功能完全相同,只是技术实现略有不同。
|
||||
|
||||
### 可以同时在 Chrome 和 Firefox 中使用吗?
|
||||
|
||||
当然可以!你可以在多个浏览器中同时安装和使用 Xget Now,它们会独立工作,各自维护自己的设置。
|
||||
|
||||
## 🌟 支持我们
|
||||
|
||||
如果你觉得这个扩展有用,请:
|
||||
|
||||
- ⭐ 为此存储库点星
|
||||
- 📝 在 [Chrome 网上应用店](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf) 留下评价
|
||||
- 🐛 通过 [GitHub Issues](https://github.com/xixu-me/Xget-for-Chromium/issues) 报告错误或建议功能
|
||||
- 📝 留下评价:
|
||||
- [Chrome 应用商店](https://chromewebstore.google.com/detail/ajiejgobfcifcikbahpijopolfjoodgf?hl=zh-CN)
|
||||
- [Firefox 附加组件](https://addons.mozilla.org/zh-CN/firefox/addon/xget-now/)
|
||||
- [Microsoft Edge 加载项](https://microsoftedge.microsoft.com/addons/detail/jigpfhbegabdenhihpplcjhpfdcgnalc?hl=zh-CN&gl=CN)
|
||||
- 🐛 通过 [GitHub Issues](https://github.com/xixu-me/Xget-Now/issues) 报告错误或建议功能
|
||||
- 📢 与其他可能受益于更快下载的人分享
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
## 支持的版本
|
||||
|
||||
我们积极支持 Xget for Chromium 的最新版本。为以下版本提供安全更新:
|
||||
我们积极支持 Xget Now 的最新版本。为以下版本提供安全更新:
|
||||
|
||||
| 版本 | 支持状态 |
|
||||
| ------- | ------------------ |
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
如果你在 Xget for Chromium 中发现安全漏洞,请负责任地报告:
|
||||
如果你在 Xget Now 中发现安全漏洞,请负责任地报告:
|
||||
|
||||
### 如何报告
|
||||
|
||||
@@ -63,4 +63,4 @@
|
||||
- 更新时审查权限
|
||||
- 报告可疑行为
|
||||
|
||||
感谢你帮助保持 Xget for Chromium 的安全!
|
||||
感谢你帮助保持 Xget Now 的安全!
|
||||
+255
-46
@@ -1,95 +1,306 @@
|
||||
// 与 worker.js 匹配的平台配置
|
||||
/**
|
||||
* Xget Now - 扩展后台脚本
|
||||
*
|
||||
* 功能:
|
||||
* - 处理扩展设置管理
|
||||
* - 监听标签页更新事件
|
||||
* - 处理来自内容脚本的消息
|
||||
* - 管理扩展状态和配置
|
||||
*/
|
||||
|
||||
// 导入兼容层
|
||||
importScripts("webext-compat.js");
|
||||
|
||||
// 确保兼容层可用
|
||||
if (typeof webext === "undefined") {
|
||||
// 在非模块环境中,兼容层应该已经通过脚本标签加载
|
||||
console.error("WebExt compatibility layer not found");
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台配置定义
|
||||
* 支持的下载加速平台列表
|
||||
*/
|
||||
const PLATFORMS = {
|
||||
// 代码托管平台
|
||||
gh: {
|
||||
base: "https://github.com",
|
||||
name: "GitHub",
|
||||
transform: (path) => path.replace(/^\/gh\//, "/"),
|
||||
pattern: /^https:\/\/github\.com\//,
|
||||
},
|
||||
gl: {
|
||||
base: "https://gitlab.com",
|
||||
name: "GitLab",
|
||||
transform: (path) => path.replace(/^\/gl\//, "/"),
|
||||
pattern: /^https:\/\/gitlab\.com\//,
|
||||
},
|
||||
gitea: {
|
||||
base: "https://gitea.com",
|
||||
name: "Gitea",
|
||||
pattern: /^https:\/\/gitea\.com\//,
|
||||
},
|
||||
codeberg: {
|
||||
base: "https://codeberg.org",
|
||||
name: "Codeberg",
|
||||
pattern: /^https:\/\/codeberg\.org\//,
|
||||
},
|
||||
sf: {
|
||||
base: "https://sourceforge.net",
|
||||
name: "SourceForge",
|
||||
pattern: /^https:\/\/sourceforge\.net\//,
|
||||
},
|
||||
aosp: {
|
||||
base: "https://android.googlesource.com",
|
||||
name: "AOSP",
|
||||
pattern: /^https:\/\/android\.googlesource\.com\//,
|
||||
},
|
||||
|
||||
// AI/ML 平台
|
||||
hf: {
|
||||
base: "https://huggingface.co",
|
||||
name: "Hugging Face",
|
||||
transform: (path) => path.replace(/^\/hf\//, "/"),
|
||||
pattern: /^https:\/\/huggingface\.co\//,
|
||||
},
|
||||
|
||||
// 包管理平台
|
||||
npm: {
|
||||
base: "https://registry.npmjs.org",
|
||||
name: "npm",
|
||||
pattern: /^https:\/\/registry\.npmjs\.org\//,
|
||||
},
|
||||
pypi: {
|
||||
base: "https://pypi.org",
|
||||
name: "PyPI",
|
||||
pattern: /^https:\/\/pypi\.org\//,
|
||||
},
|
||||
"pypi-files": {
|
||||
base: "https://files.pythonhosted.org",
|
||||
name: "PyPI Files",
|
||||
pattern: /^https:\/\/files\.pythonhosted\.org\//,
|
||||
},
|
||||
conda: {
|
||||
base: "https://repo.anaconda.com",
|
||||
name: "Conda",
|
||||
pattern: /^https:\/\/repo\.anaconda\.com\//,
|
||||
},
|
||||
"conda-community": {
|
||||
base: "https://conda.anaconda.org",
|
||||
name: "Conda Community",
|
||||
pattern: /^https:\/\/conda\.anaconda\.org\//,
|
||||
},
|
||||
maven: {
|
||||
base: "https://repo1.maven.org",
|
||||
name: "Maven",
|
||||
pattern: /^https:\/\/repo1\.maven\.org\//,
|
||||
},
|
||||
apache: {
|
||||
base: "https://downloads.apache.org",
|
||||
name: "Apache",
|
||||
pattern: /^https:\/\/downloads\.apache\.org\//,
|
||||
},
|
||||
gradle: {
|
||||
base: "https://plugins.gradle.org",
|
||||
name: "Gradle",
|
||||
pattern: /^https:\/\/plugins\.gradle\.org\//,
|
||||
},
|
||||
rubygems: {
|
||||
base: "https://rubygems.org",
|
||||
name: "RubyGems",
|
||||
pattern: /^https:\/\/rubygems\.org\//,
|
||||
},
|
||||
cran: {
|
||||
base: "https://cran.r-project.org",
|
||||
name: "CRAN",
|
||||
pattern: /^https:\/\/cran\.r-project\.org\//,
|
||||
},
|
||||
cpan: {
|
||||
base: "https://www.cpan.org",
|
||||
name: "CPAN",
|
||||
pattern: /^https:\/\/www\.cpan\.org\//,
|
||||
},
|
||||
ctan: {
|
||||
base: "https://tug.ctan.org",
|
||||
name: "CTAN",
|
||||
pattern: /^https:\/\/tug\.ctan\.org\//,
|
||||
},
|
||||
golang: {
|
||||
base: "https://proxy.golang.org",
|
||||
name: "Go Modules",
|
||||
pattern: /^https:\/\/proxy\.golang\.org\//,
|
||||
},
|
||||
nuget: {
|
||||
base: "https://api.nuget.org",
|
||||
name: "NuGet",
|
||||
pattern: /^https:\/\/api\.nuget\.org\//,
|
||||
},
|
||||
crates: {
|
||||
base: "https://crates.io",
|
||||
name: "Crates.io",
|
||||
pattern: /^https:\/\/crates\.io\//,
|
||||
},
|
||||
packagist: {
|
||||
base: "https://repo.packagist.org",
|
||||
name: "Packagist",
|
||||
pattern: /^https:\/\/repo\.packagist\.org\//,
|
||||
},
|
||||
|
||||
// 其他平台
|
||||
arxiv: {
|
||||
base: "https://arxiv.org",
|
||||
name: "arXiv",
|
||||
pattern: /^https:\/\/arxiv\.org\//,
|
||||
},
|
||||
fdroid: {
|
||||
base: "https://f-droid.org",
|
||||
name: "F-Droid",
|
||||
pattern: /^https:\/\/f-droid\.org\//,
|
||||
},
|
||||
};
|
||||
|
||||
// 默认设置
|
||||
// 转换URL为Xget格式
|
||||
function transformUrl(url, xgetDomain, enabledPlatforms) {
|
||||
try {
|
||||
const platform = detectPlatform(url);
|
||||
if (!platform || !enabledPlatforms[platform]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlObj = new URL(url);
|
||||
const path = urlObj.pathname + urlObj.search + urlObj.hash;
|
||||
|
||||
return `https://${xgetDomain}/${platform}${path}`;
|
||||
} catch (error) {
|
||||
console.error("转换 URL 时出错:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 检测平台
|
||||
function detectPlatform(url) {
|
||||
for (const [key, platform] of Object.entries(PLATFORMS)) {
|
||||
if (platform.pattern.test(url)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const DEFAULT_SETTINGS = {
|
||||
enabled: true,
|
||||
xgetDomain: "xget.xi-xu.me",
|
||||
enabledPlatforms: {
|
||||
// 代码托管平台
|
||||
gh: true,
|
||||
gl: true,
|
||||
gitea: true,
|
||||
codeberg: true,
|
||||
sf: true,
|
||||
aosp: true,
|
||||
|
||||
// AI/ML 平台
|
||||
hf: true,
|
||||
|
||||
// 包管理平台
|
||||
npm: true,
|
||||
pypi: true,
|
||||
"pypi-files": true,
|
||||
conda: true,
|
||||
"conda-community": true,
|
||||
maven: true,
|
||||
apache: true,
|
||||
gradle: true,
|
||||
rubygems: true,
|
||||
cran: true,
|
||||
cpan: true,
|
||||
ctan: true,
|
||||
golang: true,
|
||||
nuget: true,
|
||||
crates: true,
|
||||
packagist: true,
|
||||
|
||||
// 其他平台
|
||||
arxiv: true,
|
||||
fdroid: true,
|
||||
},
|
||||
};
|
||||
|
||||
// 初始化扩展
|
||||
chrome.runtime.onInstalled.addListener(async () => {
|
||||
console.log("Xget for Chromium 已安装");
|
||||
webext.runtime.onInstalled.addListener(async () => {
|
||||
console.log("Xget Now 已安装");
|
||||
|
||||
// 使用本地存储而不是同步存储以确保兼容性
|
||||
const storageAPI = webext.storage.sync || webext.storage.local;
|
||||
|
||||
// 如果尚未设置,则设置默认设置
|
||||
const settings = await chrome.storage.sync.get(DEFAULT_SETTINGS);
|
||||
await chrome.storage.sync.set(settings);
|
||||
const settings = await storageAPI.get(DEFAULT_SETTINGS);
|
||||
await storageAPI.set(settings);
|
||||
});
|
||||
|
||||
// 防止监听器被多次注册
|
||||
let downloadListenerAdded = false;
|
||||
|
||||
// 监听下载事件
|
||||
chrome.downloads.onDeterminingFilename.addListener((downloadItem, suggest) => {
|
||||
handleDownload(downloadItem, suggest);
|
||||
});
|
||||
if (!downloadListenerAdded && webext.downloads.onDeterminingFilename) {
|
||||
webext.downloads.onDeterminingFilename.addListener(handleDownload);
|
||||
downloadListenerAdded = true;
|
||||
}
|
||||
|
||||
async function handleDownload(downloadItem, suggest) {
|
||||
function handleDownload(downloadItem, suggest) {
|
||||
// 立即调用 suggest 来防止多次调用错误
|
||||
suggest();
|
||||
|
||||
// 然后异步处理重定向逻辑
|
||||
processDownloadRedirect(downloadItem);
|
||||
}
|
||||
|
||||
async function processDownloadRedirect(downloadItem) {
|
||||
try {
|
||||
const settings = await chrome.storage.sync.get(DEFAULT_SETTINGS);
|
||||
const storageAPI = webext.storage.sync || webext.storage.local;
|
||||
const settings = await storageAPI.get(DEFAULT_SETTINGS);
|
||||
|
||||
// 检查扩展是否已启用并配置了域名
|
||||
if (!settings.enabled || !settings.xgetDomain) {
|
||||
suggest();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = downloadItem.url;
|
||||
const redirectedUrl = transformUrl(url, settings);
|
||||
const redirectedUrl = transformUrl(
|
||||
url,
|
||||
settings.xgetDomain,
|
||||
settings.enabledPlatforms
|
||||
);
|
||||
|
||||
if (redirectedUrl && redirectedUrl !== url) {
|
||||
console.log("重定向下载:", url, "->", redirectedUrl);
|
||||
|
||||
// 取消原始下载
|
||||
chrome.downloads.cancel(downloadItem.id);
|
||||
|
||||
// 使用重定向的 URL 开始新下载
|
||||
chrome.downloads.download({
|
||||
url: redirectedUrl,
|
||||
filename: downloadItem.filename || undefined,
|
||||
conflictAction: "uniquify",
|
||||
});
|
||||
|
||||
// 通过内容脚本显示通知
|
||||
try {
|
||||
await chrome.tabs.sendMessage(downloadItem.tabId, {
|
||||
action: "showNotification",
|
||||
message: "下载已通过 Xget 重定向",
|
||||
// 取消原始下载
|
||||
await webext.downloads.cancel(downloadItem.id);
|
||||
|
||||
// 使用重定向的 URL 开始新下载
|
||||
await webext.downloads.download({
|
||||
url: redirectedUrl,
|
||||
filename: downloadItem.filename || undefined,
|
||||
conflictAction: "uniquify",
|
||||
});
|
||||
|
||||
// 通过内容脚本显示通知
|
||||
try {
|
||||
await webext.tabs.sendMessage(downloadItem.tabId, {
|
||||
action: "showNotification",
|
||||
message: "下载已通过 Xget 重定向",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("无法向标签页发送通知");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("无法向标签页发送通知");
|
||||
console.error("重定向下载时出错:", error);
|
||||
}
|
||||
} else {
|
||||
suggest();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("处理下载时出错:", error);
|
||||
suggest();
|
||||
console.error("处理下载重定向时出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function transformUrl(url, settings) {
|
||||
function transformUrlLegacy(url, settings) {
|
||||
try {
|
||||
// 找到匹配的平台
|
||||
for (const [platformKey, platform] of Object.entries(PLATFORMS)) {
|
||||
@@ -113,25 +324,23 @@ function transformUrl(url, settings) {
|
||||
}
|
||||
|
||||
// 监听来自弹出窗口/选项的消息
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
webext.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
const storageAPI = webext.storage.sync || webext.storage.local;
|
||||
|
||||
if (request.action === "getSettings") {
|
||||
chrome.storage.sync.get(DEFAULT_SETTINGS).then(sendResponse);
|
||||
storageAPI.get(DEFAULT_SETTINGS).then(sendResponse);
|
||||
return true;
|
||||
} else if (request.action === "saveSettings") {
|
||||
chrome.storage.sync.set(request.settings).then(async () => {
|
||||
storageAPI.set(request.settings).then(async () => {
|
||||
// 通知相关标签页刷新
|
||||
try {
|
||||
const tabs = await chrome.tabs.query({
|
||||
url: [
|
||||
"https://github.com/*",
|
||||
"https://gitlab.com/*",
|
||||
"https://huggingface.co/*",
|
||||
],
|
||||
const tabs = await webext.tabs.query({
|
||||
url: Object.values(PLATFORMS).map((platform) => platform.base + "/*"),
|
||||
});
|
||||
|
||||
for (const tab of tabs) {
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tab.id, {
|
||||
await webext.tabs.sendMessage(tab.id, {
|
||||
action: "showNotification",
|
||||
message: "设置已更新!点击刷新页面",
|
||||
showRefreshButton: true,
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
构建脚本,用于生成不同浏览器版本的扩展包
|
||||
|
||||
支持平台:
|
||||
- Chromium(Manifest V3)
|
||||
- Firefox(Manifest V2)
|
||||
|
||||
使用方法:
|
||||
python build.py --platform all --package
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def create_build_directory(platform, clean=True):
|
||||
"""
|
||||
创建构建目录
|
||||
|
||||
Args:
|
||||
platform (str): 目标平台名称 (chrome/firefox)
|
||||
clean (bool): 是否清理已存在的目录
|
||||
|
||||
Returns:
|
||||
Path: 构建目录路径
|
||||
"""
|
||||
build_dir = Path(f"build/{platform}")
|
||||
if clean and build_dir.exists():
|
||||
shutil.rmtree(build_dir)
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
return build_dir
|
||||
|
||||
|
||||
def copy_common_files(build_dir):
|
||||
"""
|
||||
复制通用文件到构建目录
|
||||
|
||||
Args:
|
||||
build_dir (Path): 目标构建目录
|
||||
"""
|
||||
common_files = [
|
||||
"webext-compat.js",
|
||||
"background.js",
|
||||
"content.js",
|
||||
"popup.js",
|
||||
"popup.html",
|
||||
"platforms.js",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"PRIVACY_POLICY.md",
|
||||
"SECURITY.md",
|
||||
]
|
||||
|
||||
for file in common_files:
|
||||
if os.path.exists(file):
|
||||
shutil.copy2(file, build_dir)
|
||||
|
||||
# 复制图标目录
|
||||
if os.path.exists("icons"):
|
||||
shutil.copytree("icons", build_dir / "icons", dirs_exist_ok=True)
|
||||
|
||||
|
||||
def build_chrome():
|
||||
"""
|
||||
构建 Chromium 版本
|
||||
|
||||
Returns:
|
||||
Path: 构建目录路径
|
||||
"""
|
||||
print("构建 Chrome 版本...")
|
||||
build_dir = create_build_directory("chrome")
|
||||
|
||||
# 复制通用文件
|
||||
copy_common_files(build_dir)
|
||||
|
||||
# 复制 Chrome manifest
|
||||
shutil.copy2("manifest.json", build_dir / "manifest.json")
|
||||
|
||||
# 修改 background.js 为 Chrome 优化
|
||||
optimize_for_chrome(build_dir)
|
||||
|
||||
print(f"Chrome 版本构建完成: {build_dir}")
|
||||
return build_dir
|
||||
|
||||
|
||||
def build_firefox():
|
||||
"""
|
||||
构建 Firefox 版本
|
||||
|
||||
Returns:
|
||||
Path: 构建目录路径
|
||||
"""
|
||||
print("构建 Firefox 版本...")
|
||||
build_dir = create_build_directory("firefox")
|
||||
|
||||
# 复制通用文件
|
||||
copy_common_files(build_dir)
|
||||
|
||||
# 复制 Firefox manifest
|
||||
shutil.copy2("manifest-firefox.json", build_dir / "manifest.json")
|
||||
|
||||
# 优化文件以适配 Firefox
|
||||
optimize_for_firefox(build_dir)
|
||||
|
||||
print(f"Firefox 版本构建完成: {build_dir}")
|
||||
return build_dir
|
||||
|
||||
|
||||
def optimize_for_chrome(build_dir):
|
||||
"""
|
||||
为 Chrome 优化文件
|
||||
|
||||
Args:
|
||||
build_dir (Path): 构建目录路径
|
||||
"""
|
||||
# 移除 background.js 中的 importScripts 调用,因为在 Manifest V3 中不需要
|
||||
bg_file = build_dir / "background.js"
|
||||
if bg_file.exists():
|
||||
content = bg_file.read_text(encoding="utf-8")
|
||||
# 注释掉 importScripts 调用
|
||||
content = content.replace(
|
||||
"importScripts('webext-compat.js');",
|
||||
"// importScripts('webext-compat.js'); // Chrome 不需要显式导入",
|
||||
)
|
||||
bg_file.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def optimize_for_firefox(build_dir):
|
||||
"""
|
||||
为 Firefox 优化文件
|
||||
|
||||
Args:
|
||||
build_dir (Path): 构建目录路径
|
||||
"""
|
||||
# Firefox 特有的优化可以在这里添加
|
||||
pass
|
||||
|
||||
|
||||
def create_package(build_dir, platform):
|
||||
"""
|
||||
创建扩展包
|
||||
|
||||
Args:
|
||||
build_dir (Path): 构建目录路径
|
||||
platform (str): 目标平台名称 (chrome/firefox)
|
||||
|
||||
Returns:
|
||||
Path: 扩展包文件路径
|
||||
"""
|
||||
package_dir = Path("packages")
|
||||
package_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 读取版本信息
|
||||
manifest_file = build_dir / "manifest.json"
|
||||
with open(manifest_file, encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
version = manifest["version"]
|
||||
|
||||
# 根据平台设置文件扩展名和命名
|
||||
if platform == "chrome":
|
||||
file_ext = "zip"
|
||||
platform_name = "chromium"
|
||||
elif platform == "firefox":
|
||||
file_ext = "xpi"
|
||||
platform_name = "firefox"
|
||||
else:
|
||||
file_ext = "zip"
|
||||
platform_name = platform
|
||||
|
||||
package_name = f"Xget-Now_{version}.{platform_name}.{file_ext}"
|
||||
package_path = package_dir / package_name
|
||||
|
||||
# 创建扩展包
|
||||
compression = zipfile.ZIP_DEFLATED
|
||||
with zipfile.ZipFile(package_path, "w", compression) as zipf:
|
||||
for file_path in build_dir.rglob("*"):
|
||||
if file_path.is_file():
|
||||
arcname = file_path.relative_to(build_dir)
|
||||
zipf.write(file_path, arcname)
|
||||
|
||||
print(f"扩展包已创建: {package_path}")
|
||||
return package_path
|
||||
|
||||
|
||||
def validate_manifest(manifest_path, platform):
|
||||
"""
|
||||
验证 manifest 文件格式和内容
|
||||
|
||||
Args:
|
||||
manifest_path (str): manifest 文件路径
|
||||
platform (str): 目标平台名称 (chrome/firefox)
|
||||
|
||||
Returns:
|
||||
bool: 验证是否通过
|
||||
"""
|
||||
try:
|
||||
with open(manifest_path, encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
if platform == "chrome":
|
||||
# 验证 Manifest V3 要求
|
||||
assert manifest.get("manifest_version") == 3, "Chrome 需要 Manifest V3"
|
||||
assert "action" in manifest, "Chrome 需要 action 字段"
|
||||
assert "service_worker" in manifest.get("background", {}), (
|
||||
"Chrome 需要 service_worker"
|
||||
)
|
||||
|
||||
elif platform == "firefox":
|
||||
# 验证 Manifest V2 要求
|
||||
assert manifest.get("manifest_version") == 2, "Firefox 需要 Manifest V2"
|
||||
assert "browser_action" in manifest, "Firefox 需要 browser_action 字段"
|
||||
assert (
|
||||
"applications" in manifest or "browser_specific_settings" in manifest
|
||||
), "Firefox 需要应用ID"
|
||||
|
||||
print(f"✓ {platform} manifest 验证通过")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ {platform} manifest 验证失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="构建 Xget Now 扩展")
|
||||
parser.add_argument(
|
||||
"--platform",
|
||||
choices=["chrome", "firefox", "all"],
|
||||
default="all",
|
||||
help="要构建的平台",
|
||||
)
|
||||
parser.add_argument("--package", action="store_true", help="创建扩展包")
|
||||
parser.add_argument(
|
||||
"--clean", action="store_true", default=True, help="清理构建目录"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
platforms = []
|
||||
if args.platform == "all":
|
||||
platforms = ["chrome", "firefox"]
|
||||
else:
|
||||
platforms = [args.platform]
|
||||
|
||||
build_results = []
|
||||
|
||||
for platform in platforms:
|
||||
try:
|
||||
if platform == "chrome":
|
||||
build_dir = build_chrome()
|
||||
elif platform == "firefox":
|
||||
build_dir = build_firefox()
|
||||
|
||||
# 验证 manifest
|
||||
manifest_path = build_dir / "manifest.json"
|
||||
if not validate_manifest(manifest_path, platform):
|
||||
continue
|
||||
|
||||
build_results.append((platform, build_dir))
|
||||
|
||||
# 创建扩展包
|
||||
if args.package:
|
||||
create_package(build_dir, platform)
|
||||
|
||||
except Exception as e:
|
||||
print(f"构建 {platform} 时出错: {e}")
|
||||
|
||||
# 总结
|
||||
print("\n构建总结:")
|
||||
for platform, build_dir in build_results:
|
||||
print(f"✓ {platform}: {build_dir}")
|
||||
|
||||
if args.package:
|
||||
print("\n扩展包位于 packages/ 目录")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+233
-6
@@ -1,7 +1,24 @@
|
||||
// 内容脚本,在支持的平台上注入下载拦截功能
|
||||
/**
|
||||
* Xget Now - 内容脚本
|
||||
*
|
||||
* 功能:
|
||||
* - 在支持的平台页面上注入下载拦截功能
|
||||
* - 监听页面上的下载链接点击事件
|
||||
* - 将下载请求重定向到加速服务
|
||||
* - 处理不同平台的特殊下载逻辑
|
||||
*/
|
||||
|
||||
// 平台检测和 URL 转换
|
||||
// 确保兼容层可用
|
||||
if (typeof webext === "undefined") {
|
||||
console.error("WebExt compatibility layer not found in content script");
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台配置定义
|
||||
* 与后台脚本保持同步的平台列表
|
||||
*/
|
||||
const PLATFORMS = {
|
||||
// 代码托管平台
|
||||
gh: {
|
||||
base: "https://github.com",
|
||||
name: "GitHub",
|
||||
@@ -12,19 +29,135 @@ const PLATFORMS = {
|
||||
name: "GitLab",
|
||||
pattern: /^https:\/\/gitlab\.com\//,
|
||||
},
|
||||
gitea: {
|
||||
base: "https://gitea.com",
|
||||
name: "Gitea",
|
||||
pattern: /^https:\/\/gitea\.com\//,
|
||||
},
|
||||
codeberg: {
|
||||
base: "https://codeberg.org",
|
||||
name: "Codeberg",
|
||||
pattern: /^https:\/\/codeberg\.org\//,
|
||||
},
|
||||
sf: {
|
||||
base: "https://sourceforge.net",
|
||||
name: "SourceForge",
|
||||
pattern: /^https:\/\/sourceforge\.net\//,
|
||||
},
|
||||
aosp: {
|
||||
base: "https://android.googlesource.com",
|
||||
name: "AOSP",
|
||||
pattern: /^https:\/\/android\.googlesource\.com\//,
|
||||
},
|
||||
|
||||
// AI/ML 平台
|
||||
hf: {
|
||||
base: "https://huggingface.co",
|
||||
name: "Hugging Face",
|
||||
pattern: /^https:\/\/huggingface\.co\//,
|
||||
},
|
||||
|
||||
// 包管理平台
|
||||
npm: {
|
||||
base: "https://registry.npmjs.org",
|
||||
name: "npm",
|
||||
pattern: /^https:\/\/registry\.npmjs\.org\//,
|
||||
},
|
||||
pypi: {
|
||||
base: "https://pypi.org",
|
||||
name: "PyPI",
|
||||
pattern: /^https:\/\/pypi\.org\//,
|
||||
},
|
||||
"pypi-files": {
|
||||
base: "https://files.pythonhosted.org",
|
||||
name: "PyPI Files",
|
||||
pattern: /^https:\/\/files\.pythonhosted\.org\//,
|
||||
},
|
||||
conda: {
|
||||
base: "https://repo.anaconda.com",
|
||||
name: "Conda",
|
||||
pattern: /^https:\/\/repo\.anaconda\.com\//,
|
||||
},
|
||||
"conda-community": {
|
||||
base: "https://conda.anaconda.org",
|
||||
name: "Conda Community",
|
||||
pattern: /^https:\/\/conda\.anaconda\.org\//,
|
||||
},
|
||||
maven: {
|
||||
base: "https://repo1.maven.org",
|
||||
name: "Maven",
|
||||
pattern: /^https:\/\/repo1\.maven\.org\//,
|
||||
},
|
||||
apache: {
|
||||
base: "https://downloads.apache.org",
|
||||
name: "Apache",
|
||||
pattern: /^https:\/\/downloads\.apache\.org\//,
|
||||
},
|
||||
gradle: {
|
||||
base: "https://plugins.gradle.org",
|
||||
name: "Gradle",
|
||||
pattern: /^https:\/\/plugins\.gradle\.org\//,
|
||||
},
|
||||
rubygems: {
|
||||
base: "https://rubygems.org",
|
||||
name: "RubyGems",
|
||||
pattern: /^https:\/\/rubygems\.org\//,
|
||||
},
|
||||
cran: {
|
||||
base: "https://cran.r-project.org",
|
||||
name: "CRAN",
|
||||
pattern: /^https:\/\/cran\.r-project\.org\//,
|
||||
},
|
||||
cpan: {
|
||||
base: "https://www.cpan.org",
|
||||
name: "CPAN",
|
||||
pattern: /^https:\/\/www\.cpan\.org\//,
|
||||
},
|
||||
ctan: {
|
||||
base: "https://tug.ctan.org",
|
||||
name: "CTAN",
|
||||
pattern: /^https:\/\/tug\.ctan\.org\//,
|
||||
},
|
||||
golang: {
|
||||
base: "https://proxy.golang.org",
|
||||
name: "Go Modules",
|
||||
pattern: /^https:\/\/proxy\.golang\.org\//,
|
||||
},
|
||||
nuget: {
|
||||
base: "https://api.nuget.org",
|
||||
name: "NuGet",
|
||||
pattern: /^https:\/\/api\.nuget\.org\//,
|
||||
},
|
||||
crates: {
|
||||
base: "https://crates.io",
|
||||
name: "Crates.io",
|
||||
pattern: /^https:\/\/crates\.io\//,
|
||||
},
|
||||
packagist: {
|
||||
base: "https://repo.packagist.org",
|
||||
name: "Packagist",
|
||||
pattern: /^https:\/\/repo\.packagist\.org\//,
|
||||
},
|
||||
|
||||
// 其他平台
|
||||
arxiv: {
|
||||
base: "https://arxiv.org",
|
||||
name: "arXiv",
|
||||
pattern: /^https:\/\/arxiv\.org\//,
|
||||
},
|
||||
fdroid: {
|
||||
base: "https://f-droid.org",
|
||||
name: "F-Droid",
|
||||
pattern: /^https:\/\/f-droid\.org\//,
|
||||
},
|
||||
};
|
||||
|
||||
// 初始化内容脚本
|
||||
(async function () {
|
||||
console.log("Xget for Chromium:内容脚本已加载");
|
||||
console.log("Xget Now:内容脚本已加载");
|
||||
|
||||
// 监听来自后台脚本的消息
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
webext.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === "showNotification") {
|
||||
showNotification(request.message, request.showRefreshButton);
|
||||
sendResponse({ success: true });
|
||||
@@ -53,7 +186,7 @@ const PLATFORMS = {
|
||||
async function getSettings() {
|
||||
try {
|
||||
return await new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage({ action: "getSettings" }, resolve);
|
||||
webext.runtime.sendMessage({ action: "getSettings" }, resolve);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取设置时出错:", error);
|
||||
@@ -221,7 +354,101 @@ function isDownloadLink(link) {
|
||||
console.error("无效的 URL:", href, e);
|
||||
}
|
||||
|
||||
// 第七检查:明确的下载文本指示器(更具体)
|
||||
// 第六检查:npm 包下载
|
||||
const allowedNpmHosts = ["registry.npmjs.org"];
|
||||
try {
|
||||
const parsedUrl = new URL(href);
|
||||
if (allowedNpmHosts.includes(parsedUrl.host)) {
|
||||
// npm tarball URL 包含 /-/
|
||||
if (pathname.includes("/-/") && pathname.endsWith(".tgz")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("无效的 URL:", href, e);
|
||||
}
|
||||
|
||||
// 第七检查:PyPI 包下载
|
||||
const allowedPypiHosts = ["pypi.org", "files.pythonhosted.org"];
|
||||
try {
|
||||
const parsedUrl = new URL(href);
|
||||
if (allowedPypiHosts.includes(parsedUrl.host)) {
|
||||
// PyPI 包文件下载
|
||||
if (
|
||||
pathname.includes("/packages/") &&
|
||||
(pathname.endsWith(".tar.gz") ||
|
||||
pathname.endsWith(".whl") ||
|
||||
pathname.endsWith(".egg"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("无效的 URL:", href, e);
|
||||
}
|
||||
|
||||
// 第八检查:SourceForge 下载
|
||||
const allowedSourceForgeHosts = ["sourceforge.net"];
|
||||
try {
|
||||
const parsedUrl = new URL(href);
|
||||
if (allowedSourceForgeHosts.includes(parsedUrl.host)) {
|
||||
// SourceForge 下载 URL 包含 /download
|
||||
if (
|
||||
pathname.includes("/download") ||
|
||||
url.searchParams.get("use_mirror")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("无效的 URL:", href, e);
|
||||
}
|
||||
|
||||
// 第九检查:Conda 包下载
|
||||
const allowedCondaHosts = ["repo.anaconda.com", "conda.anaconda.org"];
|
||||
try {
|
||||
const parsedUrl = new URL(href);
|
||||
if (allowedCondaHosts.includes(parsedUrl.host)) {
|
||||
// Conda 包文件
|
||||
if (pathname.endsWith(".conda") || pathname.endsWith(".tar.bz2")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("无效的 URL:", href, e);
|
||||
}
|
||||
|
||||
// 第十检查:其他包管理平台
|
||||
const packageManagerHosts = [
|
||||
"rubygems.org",
|
||||
"cran.r-project.org",
|
||||
"crates.io",
|
||||
"repo.packagist.org",
|
||||
"api.nuget.org",
|
||||
"proxy.golang.org",
|
||||
];
|
||||
try {
|
||||
const parsedUrl = new URL(href);
|
||||
if (packageManagerHosts.includes(parsedUrl.host)) {
|
||||
// 各种包管理器的下载文件
|
||||
const packageExtensions = [
|
||||
".gem",
|
||||
".tar.gz",
|
||||
".crate",
|
||||
".zip",
|
||||
".nupkg",
|
||||
".tgz",
|
||||
".tar.bz2",
|
||||
];
|
||||
if (packageExtensions.some((ext) => pathname.endsWith(ext))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("无效的 URL:", href, e);
|
||||
}
|
||||
|
||||
// 第十一检查:明确的下载文本指示器(更具体)
|
||||
const downloadTextIndicators = ["download", "download file", "get file"];
|
||||
const linkText = link.textContent.toLowerCase().trim();
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
开发和测试脚本
|
||||
|
||||
提供功能:
|
||||
- 启动开发服务器
|
||||
- 文件变化监控
|
||||
- 代码质量检查
|
||||
- 自动化测试
|
||||
|
||||
使用方法:
|
||||
python dev.py test # 运行测试
|
||||
python dev.py lint # 代码检查
|
||||
python dev.py watch # 监控文件变化
|
||||
python dev.py serve # 启动开发服务器
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import socketserver
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def start_dev_server(port=8000):
|
||||
"""
|
||||
启动开发服务器用于测试扩展
|
||||
|
||||
Args:
|
||||
port (int): 服务器端口号,默认 8000
|
||||
"""
|
||||
|
||||
class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=os.getcwd(), **kwargs)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
# 简化日志输出格式
|
||||
print(f"[DEV] {format % args}")
|
||||
|
||||
with socketserver.TCPServer(("", port), Handler) as httpd:
|
||||
print(f"开发服务器启动在 http://localhost:{port}")
|
||||
print("按 Ctrl+C 停止服务器")
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n开发服务器已停止")
|
||||
|
||||
|
||||
def watch_files():
|
||||
"""
|
||||
监控文件变化并自动重新构建
|
||||
|
||||
监控的文件包括:
|
||||
- manifest.json / manifest-firefox.json
|
||||
- JavaScript 文件 (background.js, content.js, popup.js)
|
||||
- HTML 文件 (popup.html)
|
||||
- 兼容层文件 (webext-compat.js)
|
||||
"""
|
||||
import os
|
||||
|
||||
watch_files = [
|
||||
"manifest.json",
|
||||
"manifest-firefox.json",
|
||||
"background.js",
|
||||
"content.js",
|
||||
"popup.js",
|
||||
"popup.html",
|
||||
"webext-compat.js",
|
||||
]
|
||||
|
||||
file_times = {}
|
||||
for file in watch_files:
|
||||
if os.path.exists(file):
|
||||
file_times[file] = os.path.getmtime(file)
|
||||
|
||||
print("开始监控文件变化...")
|
||||
print(f"监控文件: {', '.join(watch_files)}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
changed = False
|
||||
for file in watch_files:
|
||||
if os.path.exists(file):
|
||||
current_time = os.path.getmtime(file)
|
||||
if file not in file_times or current_time > file_times[file]:
|
||||
print(f"检测到文件变化: {file}")
|
||||
file_times[file] = current_time
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
print("重新构建...")
|
||||
result = subprocess.run(
|
||||
["python", "build.py", "--platform", "all"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print("✓ 构建成功")
|
||||
else:
|
||||
print(f"✗ 构建失败: {result.stderr}")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n停止文件监控")
|
||||
break
|
||||
|
||||
|
||||
def run_tests():
|
||||
"""
|
||||
运行基础测试套件
|
||||
|
||||
测试内容:
|
||||
- 必需文件存在性检查
|
||||
- Manifest 文件格式和内容验证
|
||||
- 平台特定配置检查
|
||||
|
||||
Returns:
|
||||
bool: 所有测试是否通过
|
||||
"""
|
||||
print("运行基础测试...")
|
||||
|
||||
def test_manifest(file_path, platform):
|
||||
"""
|
||||
测试 manifest 文件
|
||||
|
||||
Args:
|
||||
file_path (str): manifest 文件路径
|
||||
platform (str): 目标平台 (chrome/firefox)
|
||||
|
||||
Returns:
|
||||
bool: 测试是否通过
|
||||
"""
|
||||
try:
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
# 基础字段检查
|
||||
required_fields = ["name", "version", "manifest_version"]
|
||||
for field in required_fields:
|
||||
assert field in manifest, f"缺少必需字段: {field}"
|
||||
|
||||
# 平台特定检查
|
||||
if platform == "chrome":
|
||||
assert manifest["manifest_version"] == 3, "Chrome 需要 Manifest V3"
|
||||
assert "action" in manifest, "Chrome 需要 action 字段"
|
||||
elif platform == "firefox":
|
||||
assert manifest["manifest_version"] == 2, "Firefox 需要 Manifest V2"
|
||||
assert "browser_action" in manifest, "Firefox 需要 browser_action 字段"
|
||||
|
||||
print(f"✓ {platform} manifest 测试通过")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ {platform} manifest 测试失败: {e}")
|
||||
return False
|
||||
|
||||
# 测试文件存在性
|
||||
def test_files_exist():
|
||||
required_files = [
|
||||
"manifest.json",
|
||||
"manifest-firefox.json",
|
||||
"background.js",
|
||||
"content.js",
|
||||
"popup.js",
|
||||
"popup.html",
|
||||
"webext-compat.js",
|
||||
]
|
||||
|
||||
all_exist = True
|
||||
for file in required_files:
|
||||
if os.path.exists(file):
|
||||
print(f"✓ {file} 存在")
|
||||
else:
|
||||
print(f"✗ {file} 不存在")
|
||||
all_exist = False
|
||||
|
||||
return all_exist
|
||||
|
||||
# 运行所有测试
|
||||
results = []
|
||||
results.append(test_files_exist())
|
||||
results.append(test_manifest("manifest.json", "chrome"))
|
||||
results.append(test_manifest("manifest-firefox.json", "firefox"))
|
||||
|
||||
# 总结
|
||||
passed = sum(results)
|
||||
total = len(results)
|
||||
print(f"\n测试结果: {passed}/{total} 通过")
|
||||
|
||||
return passed == total
|
||||
|
||||
|
||||
def lint_code():
|
||||
"""
|
||||
执行代码质量检查
|
||||
|
||||
检查内容:
|
||||
- API 使用规范性 (优先使用 webext 兼容层)
|
||||
- 错误处理最佳实践
|
||||
- 接口导出规范性
|
||||
|
||||
特殊处理:
|
||||
- webext-compat.js: 允许直接使用原生 API
|
||||
- platform-detector.js: 允许直接使用原生 API
|
||||
"""
|
||||
print("运行代码检查...")
|
||||
|
||||
js_files = [
|
||||
"background.js",
|
||||
"content.js",
|
||||
"popup.js",
|
||||
"webext-compat.js",
|
||||
"platform-detector.js",
|
||||
]
|
||||
|
||||
for file in js_files:
|
||||
if os.path.exists(file):
|
||||
print(f"检查 {file}...")
|
||||
|
||||
# 读取文件内容进行检查
|
||||
with open(file, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 检查常见问题
|
||||
issues = []
|
||||
|
||||
# 底层文件允许直接使用原生 API
|
||||
bottom_layer_files = ["webext-compat.js", "platform-detector.js"]
|
||||
|
||||
if file not in bottom_layer_files:
|
||||
if "chrome." in content and "webext." not in content:
|
||||
issues.append("发现直接使用 chrome API,应使用 webext 兼容层")
|
||||
elif file == "webext-compat.js":
|
||||
# 对于 webext-compat.js,检查是否正确导出了 webext 接口
|
||||
if "window.webext" not in content and "module.exports" not in content:
|
||||
issues.append("兼容层未正确导出 webext 接口")
|
||||
elif file == "platform-detector.js":
|
||||
# 对于 platform-detector.js,检查是否正确导出了平台检测接口
|
||||
if "platformDetector" not in content:
|
||||
issues.append("平台检测器未正确定义 platformDetector 接口")
|
||||
|
||||
# 检查错误处理
|
||||
if "console.log(" in content and "console.error(" not in content:
|
||||
# 对于底层文件,console.warn 也是可以接受的
|
||||
if file in bottom_layer_files and "console.warn(" in content:
|
||||
pass # 底层文件使用 console.warn 是合理的
|
||||
else:
|
||||
issues.append("建议使用 console.error 处理错误")
|
||||
|
||||
if issues:
|
||||
print(f" ⚠ {file} 发现问题:")
|
||||
for issue in issues:
|
||||
print(f" - {issue}")
|
||||
else:
|
||||
print(f" ✓ {file} 无明显问题")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Xget Now 开发工具")
|
||||
parser.add_argument(
|
||||
"command", choices=["test", "lint", "watch", "serve"], help="要执行的命令"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port", type=int, default=8000, help="开发服务器端口 (默认: 8000)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "test":
|
||||
success = run_tests()
|
||||
exit(0 if success else 1)
|
||||
elif args.command == "lint":
|
||||
lint_code()
|
||||
elif args.command == "watch":
|
||||
watch_files()
|
||||
elif args.command == "serve":
|
||||
start_dev_server(args.port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Xget Now",
|
||||
"version": "2.0.3",
|
||||
"description": "为 Firefox 浏览器提供的 Xget 下载加速扩展,支持 GitHub、GitLab、npm、PyPI 等众多平台的文件下载加速",
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Xget Now"
|
||||
},
|
||||
"background": {
|
||||
"scripts": [
|
||||
"webext-compat.js",
|
||||
"background.js"
|
||||
],
|
||||
"persistent": false
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": [
|
||||
"webext-compat.js",
|
||||
"content.js"
|
||||
],
|
||||
"matches": [
|
||||
"https://github.com/*",
|
||||
"https://gitlab.com/*",
|
||||
"https://gitea.com/*",
|
||||
"https://codeberg.org/*",
|
||||
"https://sourceforge.net/*",
|
||||
"https://android.googlesource.com/*",
|
||||
"https://huggingface.co/*",
|
||||
"https://registry.npmjs.org/*",
|
||||
"https://pypi.org/*",
|
||||
"https://files.pythonhosted.org/*",
|
||||
"https://repo.anaconda.com/*",
|
||||
"https://conda.anaconda.org/*",
|
||||
"https://repo1.maven.org/*",
|
||||
"https://downloads.apache.org/*",
|
||||
"https://plugins.gradle.org/*",
|
||||
"https://rubygems.org/*",
|
||||
"https://cran.r-project.org/*",
|
||||
"https://www.cpan.org/*",
|
||||
"https://tug.ctan.org/*",
|
||||
"https://proxy.golang.org/*",
|
||||
"https://api.nuget.org/*",
|
||||
"https://crates.io/*",
|
||||
"https://repo.packagist.org/*",
|
||||
"https://arxiv.org/*",
|
||||
"https://f-droid.org/*"
|
||||
],
|
||||
"run_at": "document_end"
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"downloads",
|
||||
"storage",
|
||||
"activeTab",
|
||||
"https://github.com/*",
|
||||
"https://gitlab.com/*",
|
||||
"https://gitea.com/*",
|
||||
"https://codeberg.org/*",
|
||||
"https://sourceforge.net/*",
|
||||
"https://android.googlesource.com/*",
|
||||
"https://huggingface.co/*",
|
||||
"https://registry.npmjs.org/*",
|
||||
"https://pypi.org/*",
|
||||
"https://files.pythonhosted.org/*",
|
||||
"https://repo.anaconda.com/*",
|
||||
"https://conda.anaconda.org/*",
|
||||
"https://repo1.maven.org/*",
|
||||
"https://downloads.apache.org/*",
|
||||
"https://plugins.gradle.org/*",
|
||||
"https://rubygems.org/*",
|
||||
"https://cran.r-project.org/*",
|
||||
"https://www.cpan.org/*",
|
||||
"https://tug.ctan.org/*",
|
||||
"https://proxy.golang.org/*",
|
||||
"https://api.nuget.org/*",
|
||||
"https://crates.io/*",
|
||||
"https://repo.packagist.org/*",
|
||||
"https://arxiv.org/*",
|
||||
"https://f-droid.org/*"
|
||||
],
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
"icons/*"
|
||||
],
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "i@xi-xu.me",
|
||||
"strict_min_version": "109.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
-6
@@ -7,7 +7,7 @@
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Xget for Chromium"
|
||||
"default_title": "Xget Now"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
@@ -15,21 +15,66 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": [
|
||||
"webext-compat.js",
|
||||
"content.js"
|
||||
],
|
||||
"matches": [
|
||||
"https://github.com/*",
|
||||
"https://gitlab.com/*",
|
||||
"https://huggingface.co/*"
|
||||
"https://gitea.com/*",
|
||||
"https://codeberg.org/*",
|
||||
"https://sourceforge.net/*",
|
||||
"https://android.googlesource.com/*",
|
||||
"https://huggingface.co/*",
|
||||
"https://registry.npmjs.org/*",
|
||||
"https://pypi.org/*",
|
||||
"https://files.pythonhosted.org/*",
|
||||
"https://repo.anaconda.com/*",
|
||||
"https://conda.anaconda.org/*",
|
||||
"https://repo1.maven.org/*",
|
||||
"https://downloads.apache.org/*",
|
||||
"https://plugins.gradle.org/*",
|
||||
"https://rubygems.org/*",
|
||||
"https://cran.r-project.org/*",
|
||||
"https://www.cpan.org/*",
|
||||
"https://tug.ctan.org/*",
|
||||
"https://proxy.golang.org/*",
|
||||
"https://api.nuget.org/*",
|
||||
"https://crates.io/*",
|
||||
"https://repo.packagist.org/*",
|
||||
"https://arxiv.org/*",
|
||||
"https://f-droid.org/*"
|
||||
],
|
||||
"run_at": "document_end"
|
||||
}
|
||||
],
|
||||
"description": "为 GitHub、GitLab 和 Hugging Face 提供更快的文件下载体验",
|
||||
"description": "为 Chromium 浏览器提供的 Xget 下载加速扩展,支持 GitHub、GitLab、npm、PyPI 等众多平台的文件下载加速",
|
||||
"host_permissions": [
|
||||
"https://github.com/*",
|
||||
"https://gitlab.com/*",
|
||||
"https://huggingface.co/*"
|
||||
"https://gitea.com/*",
|
||||
"https://codeberg.org/*",
|
||||
"https://sourceforge.net/*",
|
||||
"https://android.googlesource.com/*",
|
||||
"https://huggingface.co/*",
|
||||
"https://registry.npmjs.org/*",
|
||||
"https://pypi.org/*",
|
||||
"https://files.pythonhosted.org/*",
|
||||
"https://repo.anaconda.com/*",
|
||||
"https://conda.anaconda.org/*",
|
||||
"https://repo1.maven.org/*",
|
||||
"https://downloads.apache.org/*",
|
||||
"https://plugins.gradle.org/*",
|
||||
"https://rubygems.org/*",
|
||||
"https://cran.r-project.org/*",
|
||||
"https://www.cpan.org/*",
|
||||
"https://tug.ctan.org/*",
|
||||
"https://proxy.golang.org/*",
|
||||
"https://api.nuget.org/*",
|
||||
"https://crates.io/*",
|
||||
"https://repo.packagist.org/*",
|
||||
"https://arxiv.org/*",
|
||||
"https://f-droid.org/*"
|
||||
],
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
@@ -38,13 +83,13 @@
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"manifest_version": 3,
|
||||
"name": "Xget for Chromium",
|
||||
"name": "Xget Now",
|
||||
"permissions": [
|
||||
"downloads",
|
||||
"storage",
|
||||
"activeTab"
|
||||
],
|
||||
"version": "1.3.0",
|
||||
"version": "2.0.3",
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"matches": [
|
||||
|
||||
+45
-21
@@ -1,39 +1,63 @@
|
||||
{
|
||||
"author": "Xi Xu",
|
||||
"browser": "chrome",
|
||||
"bugs": {
|
||||
"url": "https://github.com/xixu-me/Xget-for-Chromium/issues"
|
||||
"url": "https://github.com/xixu-me/Xget-Now/issues"
|
||||
},
|
||||
"dependencies": {},
|
||||
"description": "为 GitHub、GitLab 和 Hugging Face 提供更快的文件下载体验",
|
||||
"devDependencies": {},
|
||||
"homepage": "https://github.com/xixu-me/Xget-for-Chromium#readme",
|
||||
"description": "为 Chromium 浏览器提供的 Xget 下载加速扩展,支持 GitHub、GitLab、npm、PyPI 等众多平台的文件下载加速",
|
||||
"engines": {
|
||||
"chrome": ">=88"
|
||||
},
|
||||
"homepage": "https://github.com/xixu-me/Xget-Now",
|
||||
"keywords": [
|
||||
"chrome-extension",
|
||||
"download-accelerator",
|
||||
"xget",
|
||||
"proxy",
|
||||
"github",
|
||||
"gitlab",
|
||||
"huggingface"
|
||||
"npm",
|
||||
"pypi",
|
||||
"xget",
|
||||
"download-proxy"
|
||||
],
|
||||
"license": "GPL-3.0",
|
||||
"main": "background.js",
|
||||
"manifest_version": 3,
|
||||
"name": "xget-for-chromium",
|
||||
"manifest": {
|
||||
"platforms": [
|
||||
"github.com",
|
||||
"gitlab.com",
|
||||
"gitea.com",
|
||||
"codeberg.org",
|
||||
"sourceforge.net",
|
||||
"android.googlesource.com",
|
||||
"huggingface.co",
|
||||
"registry.npmjs.org",
|
||||
"pypi.org",
|
||||
"files.pythonhosted.org",
|
||||
"repo.anaconda.com",
|
||||
"conda.anaconda.org",
|
||||
"repo1.maven.org",
|
||||
"downloads.apache.org",
|
||||
"plugins.gradle.org",
|
||||
"rubygems.org",
|
||||
"cran.r-project.org",
|
||||
"www.cpan.org",
|
||||
"tug.ctan.org",
|
||||
"proxy.golang.org",
|
||||
"api.nuget.org",
|
||||
"crates.io",
|
||||
"repo.packagist.org",
|
||||
"arxiv.org",
|
||||
"f-droid.org"
|
||||
],
|
||||
"version": 3
|
||||
},
|
||||
"name": "xget-now",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/xixu-me/Xget-for-Chromium.git"
|
||||
"url": "https://github.com/xixu-me/Xget-Now.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "不需要构建过程 - 扩展直接从源码运行",
|
||||
"clean": "rm -f *.zip",
|
||||
"dev": "打开 chrome://extensions/ 并加载解压的扩展",
|
||||
"lint": "未配置代码检查 - 建议为 JavaScript 文件添加 ESLint",
|
||||
"package": "zip -r extension.zip . -x '*.git*' 'node_modules/*' '.github/*' '*.md' 'LICENSE' '*.zip'",
|
||||
"release": "npm run validate && npm run package",
|
||||
"test": "在 Chrome 开发者模式下加载扩展进行测试",
|
||||
"validate": "node -e \"JSON.parse(require('fs').readFileSync('manifest.json', 'utf8')); console.log('✅ manifest.json 格式有效')\""
|
||||
"build": "echo 'No build process needed for Chrome extension'",
|
||||
"test": "echo 'No tests specified'"
|
||||
},
|
||||
"version": "1.3.0"
|
||||
"version": "2.0.3"
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 平台特定的配置和检测
|
||||
*
|
||||
* 提供浏览器平台检测和特性识别功能:
|
||||
* - 检测当前运行的浏览器类型和版本
|
||||
* - 识别 Manifest 版本 (V2/V3)
|
||||
* - 检测移动端环境
|
||||
* - 识别开发版本
|
||||
*/
|
||||
|
||||
/**
|
||||
* 平台检测器
|
||||
* 提供浏览器环境检测的核心功能
|
||||
*/
|
||||
const platformDetector = {
|
||||
/**
|
||||
* 检测当前浏览器平台
|
||||
*
|
||||
* @returns {Object} 平台检测结果
|
||||
* @property {string} platform - 浏览器类型 (firefox/chrome/edge/opera/safari/unknown)
|
||||
* @property {number} version - 浏览器主版本号
|
||||
* @property {number} manifestVersion - Manifest 版本号
|
||||
* @property {boolean} isMobile - 是否为移动端
|
||||
* @property {boolean} isDevBuild - 是否为开发版本
|
||||
*/
|
||||
detect() {
|
||||
const manifest =
|
||||
typeof browser !== "undefined" && browser.runtime
|
||||
? browser.runtime.getManifest()
|
||||
: chrome.runtime.getManifest();
|
||||
|
||||
const ua = navigator.userAgent;
|
||||
const extURL =
|
||||
typeof browser !== "undefined" && browser.runtime
|
||||
? browser.runtime.getURL("")
|
||||
: chrome.runtime.getURL("");
|
||||
|
||||
let platform = "unknown";
|
||||
let version = 0;
|
||||
|
||||
// 根据扩展 URL 前缀检测浏览器类型
|
||||
if (extURL.startsWith("moz-extension://")) {
|
||||
platform = "firefox";
|
||||
const match = /Firefox\/(\d+)/.exec(ua);
|
||||
version = match ? parseInt(match[1], 10) : 115;
|
||||
} else if (extURL.startsWith("safari-web-extension://")) {
|
||||
platform = "safari";
|
||||
const match = /Version\/(\d+)/.exec(ua);
|
||||
version = match ? parseInt(match[1], 10) : 15;
|
||||
} else if (/\bEdg\/\b/.test(ua)) {
|
||||
platform = "edge";
|
||||
const match = /Edg\/(\d+)/.exec(ua);
|
||||
version = match ? parseInt(match[1], 10) : 120;
|
||||
} else if (/\bOPR\/\b/.test(ua)) {
|
||||
platform = "opera";
|
||||
const match = /OPR\/(\d+)/.exec(ua);
|
||||
version = match ? parseInt(match[1], 10) : 120;
|
||||
} else {
|
||||
platform = "chrome";
|
||||
const match = /Chrome\/(\d+)/.exec(ua);
|
||||
version = match ? parseInt(match[1], 10) : 120;
|
||||
}
|
||||
|
||||
return {
|
||||
platform,
|
||||
version,
|
||||
manifestVersion: manifest.manifest_version,
|
||||
isMobile: /\bMobile\b/.test(ua),
|
||||
isDevBuild: /^\d+\.\d+\.\d+\D/.test(manifest.version),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取平台特定的能力
|
||||
*/
|
||||
getCapabilities(platformInfo = null) {
|
||||
const info = platformInfo || this.detect();
|
||||
|
||||
const capabilities = {
|
||||
// 基础能力
|
||||
downloads: true,
|
||||
storage: true,
|
||||
tabs: true,
|
||||
|
||||
// 高级能力
|
||||
declarativeNetRequest: false,
|
||||
offscreen: false,
|
||||
scripting: false,
|
||||
|
||||
// 特殊能力
|
||||
popupResize: true,
|
||||
contextMenus: true,
|
||||
notifications: true,
|
||||
};
|
||||
|
||||
// Firefox 特定能力
|
||||
if (info.platform === "firefox") {
|
||||
capabilities.webRequest = true;
|
||||
capabilities.contentSecurityPolicy = false; // 在 Manifest V2 中有限制
|
||||
capabilities.popupResize = false; // Firefox 不支持动态调整弹窗大小
|
||||
}
|
||||
|
||||
// Chromium 特定能力
|
||||
if (
|
||||
info.platform === "chrome" ||
|
||||
info.platform === "edge" ||
|
||||
info.platform === "opera"
|
||||
) {
|
||||
if (info.manifestVersion === 3) {
|
||||
capabilities.declarativeNetRequest = true;
|
||||
capabilities.offscreen = info.version >= 109;
|
||||
capabilities.scripting = true;
|
||||
capabilities.webRequest = false; // MV3 中 webRequest 被限制
|
||||
} else {
|
||||
capabilities.webRequest = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Safari 特定能力
|
||||
if (info.platform === "safari") {
|
||||
capabilities.notifications = false; // Safari 扩展通知支持有限
|
||||
capabilities.contextMenus = false; // Safari 扩展上下文菜单支持有限
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取平台特定的限制
|
||||
*/
|
||||
getLimitations(platformInfo = null) {
|
||||
const info = platformInfo || this.detect();
|
||||
|
||||
const limitations = {
|
||||
maxStorageSize: Infinity,
|
||||
maxDownloadConcurrency: 10,
|
||||
requiresUserGesture: false,
|
||||
contentScriptLimitations: [],
|
||||
};
|
||||
|
||||
if (info.platform === "firefox") {
|
||||
limitations.maxStorageSize = 5 * 1024 * 1024; // 5MB for local storage
|
||||
limitations.contentScriptLimitations = [
|
||||
"limited-eval", // eval() 受限
|
||||
"no-dynamic-import", // 动态导入受限
|
||||
];
|
||||
}
|
||||
|
||||
if (info.platform === "safari") {
|
||||
limitations.maxStorageSize = 10 * 1024 * 1024; // 10MB
|
||||
limitations.requiresUserGesture = true; // 许多操作需要用户手势
|
||||
limitations.maxDownloadConcurrency = 3; // 下载并发限制更严格
|
||||
}
|
||||
|
||||
return limitations;
|
||||
},
|
||||
};
|
||||
|
||||
// 导出
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = { platformDetector };
|
||||
} else {
|
||||
window.platformDetector = platformDetector;
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 平台配置对象,包含各个平台的基础URL和名称
|
||||
* @type {Object.<string, {base: string, name: string, pattern: RegExp}>}
|
||||
*/
|
||||
export const PLATFORMS = {
|
||||
// 代码托管平台
|
||||
gh: {
|
||||
base: "https://github.com",
|
||||
name: "GitHub",
|
||||
pattern: /^https:\/\/github\.com\//,
|
||||
},
|
||||
gl: {
|
||||
base: "https://gitlab.com",
|
||||
name: "GitLab",
|
||||
pattern: /^https:\/\/gitlab\.com\//,
|
||||
},
|
||||
gitea: {
|
||||
base: "https://gitea.com",
|
||||
name: "Gitea",
|
||||
pattern: /^https:\/\/gitea\.com\//,
|
||||
},
|
||||
codeberg: {
|
||||
base: "https://codeberg.org",
|
||||
name: "Codeberg",
|
||||
pattern: /^https:\/\/codeberg\.org\//,
|
||||
},
|
||||
sf: {
|
||||
base: "https://sourceforge.net",
|
||||
name: "SourceForge",
|
||||
pattern: /^https:\/\/sourceforge\.net\//,
|
||||
},
|
||||
aosp: {
|
||||
base: "https://android.googlesource.com",
|
||||
name: "AOSP",
|
||||
pattern: /^https:\/\/android\.googlesource\.com\//,
|
||||
},
|
||||
|
||||
// AI/ML 平台
|
||||
hf: {
|
||||
base: "https://huggingface.co",
|
||||
name: "Hugging Face",
|
||||
pattern: /^https:\/\/huggingface\.co\//,
|
||||
},
|
||||
|
||||
// 包管理平台
|
||||
npm: {
|
||||
base: "https://registry.npmjs.org",
|
||||
name: "npm",
|
||||
pattern: /^https:\/\/registry\.npmjs\.org\//,
|
||||
},
|
||||
pypi: {
|
||||
base: "https://pypi.org",
|
||||
name: "PyPI",
|
||||
pattern: /^https:\/\/pypi\.org\//,
|
||||
},
|
||||
"pypi-files": {
|
||||
base: "https://files.pythonhosted.org",
|
||||
name: "PyPI Files",
|
||||
pattern: /^https:\/\/files\.pythonhosted\.org\//,
|
||||
},
|
||||
conda: {
|
||||
base: "https://repo.anaconda.com",
|
||||
name: "Conda",
|
||||
pattern: /^https:\/\/repo\.anaconda\.com\//,
|
||||
},
|
||||
"conda-community": {
|
||||
base: "https://conda.anaconda.org",
|
||||
name: "Conda Community",
|
||||
pattern: /^https:\/\/conda\.anaconda\.org\//,
|
||||
},
|
||||
maven: {
|
||||
base: "https://repo1.maven.org",
|
||||
name: "Maven",
|
||||
pattern: /^https:\/\/repo1\.maven\.org\//,
|
||||
},
|
||||
apache: {
|
||||
base: "https://downloads.apache.org",
|
||||
name: "Apache",
|
||||
pattern: /^https:\/\/downloads\.apache\.org\//,
|
||||
},
|
||||
gradle: {
|
||||
base: "https://plugins.gradle.org",
|
||||
name: "Gradle",
|
||||
pattern: /^https:\/\/plugins\.gradle\.org\//,
|
||||
},
|
||||
rubygems: {
|
||||
base: "https://rubygems.org",
|
||||
name: "RubyGems",
|
||||
pattern: /^https:\/\/rubygems\.org\//,
|
||||
},
|
||||
cran: {
|
||||
base: "https://cran.r-project.org",
|
||||
name: "CRAN",
|
||||
pattern: /^https:\/\/cran\.r-project\.org\//,
|
||||
},
|
||||
cpan: {
|
||||
base: "https://www.cpan.org",
|
||||
name: "CPAN",
|
||||
pattern: /^https:\/\/www\.cpan\.org\//,
|
||||
},
|
||||
ctan: {
|
||||
base: "https://tug.ctan.org",
|
||||
name: "CTAN",
|
||||
pattern: /^https:\/\/tug\.ctan\.org\//,
|
||||
},
|
||||
golang: {
|
||||
base: "https://proxy.golang.org",
|
||||
name: "Go Modules",
|
||||
pattern: /^https:\/\/proxy\.golang\.org\//,
|
||||
},
|
||||
nuget: {
|
||||
base: "https://api.nuget.org",
|
||||
name: "NuGet",
|
||||
pattern: /^https:\/\/api\.nuget\.org\//,
|
||||
},
|
||||
crates: {
|
||||
base: "https://crates.io",
|
||||
name: "Crates.io",
|
||||
pattern: /^https:\/\/crates\.io\//,
|
||||
},
|
||||
packagist: {
|
||||
base: "https://repo.packagist.org",
|
||||
name: "Packagist",
|
||||
pattern: /^https:\/\/repo\.packagist\.org\//,
|
||||
},
|
||||
|
||||
// 其他平台
|
||||
arxiv: {
|
||||
base: "https://arxiv.org",
|
||||
name: "arXiv",
|
||||
pattern: /^https:\/\/arxiv\.org\//,
|
||||
},
|
||||
fdroid: {
|
||||
base: "https://f-droid.org",
|
||||
name: "F-Droid",
|
||||
pattern: /^https:\/\/f-droid\.org\//,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一的路径转换函数
|
||||
* @param {string} path - 原始路径
|
||||
* @param {string} platformKey - 平台键值
|
||||
* @returns {string} - 转换后的路径
|
||||
*/
|
||||
export function transformPath(path, platformKey) {
|
||||
if (!PLATFORMS[platformKey]) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const prefix = `/${platformKey.replace(/-/g, "/")}/`;
|
||||
let transformedPath = path.replace(
|
||||
new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`),
|
||||
"/"
|
||||
);
|
||||
|
||||
// 特殊处理 crates.io API 路径
|
||||
if (platformKey === "crates") {
|
||||
// 转换路径以包含 API 前缀
|
||||
if (transformedPath.startsWith("/")) {
|
||||
// 处理不同的 API 端点:
|
||||
// /serde/1.0.0/download -> /api/v1/crates/serde/1.0.0/download
|
||||
// /serde -> /api/v1/crates/serde
|
||||
// /?q=query -> /api/v1/crates?q=query
|
||||
if (transformedPath === "/" || transformedPath.startsWith("/?")) {
|
||||
// 搜索端点
|
||||
transformedPath = transformedPath.replace("/", "/api/v1/crates");
|
||||
} else {
|
||||
// 特定 crate 端点
|
||||
transformedPath = `/api/v1/crates${transformedPath}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transformedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据URL检测平台
|
||||
* @param {string} url - 要检测的URL
|
||||
* @returns {string|null} - 平台键值或null
|
||||
*/
|
||||
export function detectPlatform(url) {
|
||||
for (const [key, platform] of Object.entries(PLATFORMS)) {
|
||||
if (platform.pattern.test(url)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换URL为Xget格式
|
||||
* @param {string} url - 原始URL
|
||||
* @param {string} xgetDomain - Xget域名
|
||||
* @param {Object} enabledPlatforms - 启用的平台配置
|
||||
* @returns {string|null} - 转换后的URL或null
|
||||
*/
|
||||
export function transformUrl(url, xgetDomain, enabledPlatforms) {
|
||||
try {
|
||||
const platform = detectPlatform(url);
|
||||
if (!platform || !enabledPlatforms[platform]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlObj = new URL(url);
|
||||
const path = urlObj.pathname + urlObj.search + urlObj.hash;
|
||||
const transformedPath = transformPath(path, platform);
|
||||
|
||||
return `https://${xgetDomain}/${platform}${transformedPath}`;
|
||||
} catch (error) {
|
||||
console.error("转换 URL 时出错:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+375
-89
@@ -3,61 +3,161 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Xget for Chromium</title>
|
||||
<title>Xget Now</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-accent-color: #8a2be2;
|
||||
--success-color: #4caf50;
|
||||
--container-bg: rgba(255, 255, 255, 0.1);
|
||||
--container-border: rgba(255, 255, 255, 0.2);
|
||||
--text-color: white;
|
||||
--text-secondary-color: rgba(255, 255, 255, 0.7);
|
||||
--input-bg: rgba(0, 0, 0, 0.2);
|
||||
--hover-bg: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
html {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
overflow: hidden; /* Prevents scrollbars from appearing on the html element */
|
||||
}
|
||||
|
||||
body {
|
||||
width: 380px;
|
||||
min-height: 280px;
|
||||
min-height: 400px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
color: var(--text-color);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
background-color: rgba(40, 40, 40, 0.3);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 1px solid var(--container-border);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header .subtitle {
|
||||
margin: 5px 0 0 0;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
max-height: calc(400px - 70px); /* Fixed height instead of viewport */
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Enhanced Custom Scrollbar */
|
||||
.content::-webkit-scrollbar,
|
||||
.platforms::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar-track,
|
||||
.platforms::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar-thumb,
|
||||
.platforms::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar-thumb:hover,
|
||||
.platforms::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar-thumb:active,
|
||||
.platforms::-webkit-scrollbar-thumb:active {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* Scrollbar corner for better appearance */
|
||||
.content::-webkit-scrollbar-corner,
|
||||
.platforms::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Smooth scrolling support for Firefox */
|
||||
.content,
|
||||
.platforms {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||||
}
|
||||
|
||||
/* Auto-hide scrollbar behavior */
|
||||
.content:not(:hover):not(:focus-within),
|
||||
.platforms:not(:hover):not(:focus-within) {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.content:not(:hover):not(:focus-within)::-webkit-scrollbar,
|
||||
.platforms:not(:hover):not(:focus-within)::-webkit-scrollbar {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
margin-bottom: 25px;
|
||||
padding: 15px;
|
||||
background: var(--container-bg);
|
||||
border: 1px solid var(--container-border);
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.setting-group.compact {
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.setting-group:last-child {
|
||||
margin-bottom: 0;
|
||||
.setting-group:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
|
||||
.setting-label.compact {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
@@ -73,101 +173,160 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transition: 0.3s;
|
||||
border-radius: 24px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
transition: 0.4s;
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
transition: 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #4caf50;
|
||||
background-color: var(--primary-accent-color);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
.domain-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
padding: 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.domain-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
|
||||
.domain-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: var(--primary-accent-color);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 0 0 3px rgba(138, 43, 226, 0.3);
|
||||
}
|
||||
|
||||
.platforms {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-top: 8px;
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.platform-category {
|
||||
padding: 8px 12px 0 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary-color);
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.platform-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.platform-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.platform-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
border: 1px solid transparent;
|
||||
animation: status-pop-in 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes status-pop-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.status.hiding {
|
||||
animation: fadeOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
background: rgba(76, 175, 80, 0.25);
|
||||
border-color: rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
background: rgba(244, 67, 54, 0.25);
|
||||
border-color: rgba(244, 67, 54, 0.4);
|
||||
}
|
||||
|
||||
.mini-toggle {
|
||||
width: 32px;
|
||||
height: 16px;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.mini-toggle .slider {
|
||||
border-radius: 16px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.mini-toggle .slider:before {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
}
|
||||
|
||||
.mini-toggle input:checked + .slider:before {
|
||||
@@ -177,55 +336,56 @@
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.link-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 12px 15px;
|
||||
background: var(--container-bg);
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
color: var(--text-color);
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--container-border);
|
||||
}
|
||||
|
||||
.link-item:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
background: var(--hover-bg);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
margin-right: 12px;
|
||||
font-size: 18px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Xget for Chromium</h1>
|
||||
<div class="subtitle">下载加速器</div>
|
||||
<h1>Xget Now</h1>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">
|
||||
<div class="setting-group compact">
|
||||
<label class="setting-label compact">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 28px;
|
||||
"
|
||||
>
|
||||
启用扩展
|
||||
<span>启用扩展</span>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="enabledToggle" />
|
||||
<span class="slider"></span>
|
||||
@@ -240,33 +400,154 @@
|
||||
type="text"
|
||||
id="domainInput"
|
||||
class="domain-input"
|
||||
placeholder="你的-xget-域名.com"
|
||||
placeholder="your-xget-domain.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">启用的平台</label>
|
||||
<div class="platforms">
|
||||
<div class="platform-item">
|
||||
<span>GitHub</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="ghToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<div class="platform-category">
|
||||
<div class="category-title">代码托管</div>
|
||||
<div class="platform-grid">
|
||||
<div class="platform-item">
|
||||
<span>GitHub</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="ghToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>GitLab</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="glToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>Gitea</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="giteaToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>Codeberg</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="codebergToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>SourceForge</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="sfToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>AOSP</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="aospToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>GitLab</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="glToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
|
||||
<div class="platform-category">
|
||||
<div class="category-title">AI/ML</div>
|
||||
<div class="platform-grid">
|
||||
<div class="platform-item">
|
||||
<span>Hugging Face</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="hfToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>Hugging Face</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="hfToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
|
||||
<div class="platform-category">
|
||||
<div class="category-title">包管理</div>
|
||||
<div class="platform-grid">
|
||||
<div class="platform-item">
|
||||
<span>npm</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="npmToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>PyPI</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="pypiToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>Conda</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="condaToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>Maven</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="mavenToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>RubyGems</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="rubygemsToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>Crates.io</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="cratesToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>NuGet</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="nugetToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>Go Modules</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="golangToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="platform-category">
|
||||
<div class="category-title">其他</div>
|
||||
<div class="platform-grid">
|
||||
<div class="platform-item">
|
||||
<span>arXiv</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="arxivToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="platform-item">
|
||||
<span>F-Droid</span>
|
||||
<label class="toggle-switch mini-toggle">
|
||||
<input type="checkbox" id="fdroidToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -279,15 +560,19 @@
|
||||
class="link-item"
|
||||
>
|
||||
<span class="link-icon">🚀</span>
|
||||
<span class="link-text">部署你自己的 Xget 服务</span>
|
||||
<span class="link-text">Xget 存储库</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/xixu-me/Xget-for-Chromium"
|
||||
href="https://github.com/xixu-me/Xget-Now"
|
||||
target="_blank"
|
||||
class="link-item"
|
||||
>
|
||||
<span class="link-icon">📖</span>
|
||||
<span class="link-text">源码和文档</span>
|
||||
<span class="link-text">扩展存储库</span>
|
||||
</a>
|
||||
<a href="https://xuc.xi-xu.me/" target="_blank" class="link-item">
|
||||
<span class="link-icon">🔗</span>
|
||||
<span class="link-text">URL 转换器</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,6 +580,7 @@
|
||||
<div id="status" class="status" style="display: none"></div>
|
||||
</div>
|
||||
|
||||
<script src="webext-compat.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,20 @@
|
||||
/**
|
||||
* Xget Now - 弹窗脚本
|
||||
*
|
||||
* 功能:
|
||||
* - 管理扩展设置界面
|
||||
* - 处理用户交互事件
|
||||
* - 同步设置到后台脚本
|
||||
* - 提供平台开关控制
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// 确保兼容层可用
|
||||
if (typeof webext === "undefined") {
|
||||
console.error("WebExt compatibility layer not found in popup");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载当前设置
|
||||
await loadSettings();
|
||||
|
||||
@@ -6,9 +22,12 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载并应用当前设置到 UI
|
||||
*/
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const settings = await chrome.runtime.sendMessage({
|
||||
const settings = await webext.runtime.sendMessage({
|
||||
action: "getSettings",
|
||||
});
|
||||
|
||||
@@ -20,10 +39,44 @@ async function loadSettings() {
|
||||
const enabledValue = settings.enabled && domainValue.trim() !== "";
|
||||
document.getElementById("enabledToggle").checked = enabledValue;
|
||||
|
||||
// 更新平台切换
|
||||
document.getElementById("ghToggle").checked = settings.enabledPlatforms.gh;
|
||||
document.getElementById("glToggle").checked = settings.enabledPlatforms.gl;
|
||||
document.getElementById("hfToggle").checked = settings.enabledPlatforms.hf;
|
||||
// 更新平台切换状态
|
||||
const platformToggles = {
|
||||
// 代码托管平台
|
||||
gh: document.getElementById("ghToggle"),
|
||||
gl: document.getElementById("glToggle"),
|
||||
gitea: document.getElementById("giteaToggle"),
|
||||
codeberg: document.getElementById("codebergToggle"),
|
||||
sf: document.getElementById("sfToggle"),
|
||||
aosp: document.getElementById("aospToggle"),
|
||||
|
||||
// AI/ML 平台
|
||||
hf: document.getElementById("hfToggle"),
|
||||
|
||||
// 包管理平台
|
||||
npm: document.getElementById("npmToggle"),
|
||||
pypi: document.getElementById("pypiToggle"),
|
||||
conda: document.getElementById("condaToggle"),
|
||||
maven: document.getElementById("mavenToggle"),
|
||||
rubygems: document.getElementById("rubygemsToggle"),
|
||||
crates: document.getElementById("cratesToggle"),
|
||||
nuget: document.getElementById("nugetToggle"),
|
||||
golang: document.getElementById("golangToggle"),
|
||||
|
||||
// 其他平台
|
||||
arxiv: document.getElementById("arxivToggle"),
|
||||
fdroid: document.getElementById("fdroidToggle"),
|
||||
};
|
||||
|
||||
// 设置平台切换状态
|
||||
Object.entries(platformToggles).forEach(([platform, toggle]) => {
|
||||
if (
|
||||
toggle &&
|
||||
settings.enabledPlatforms &&
|
||||
settings.enabledPlatforms[platform] !== undefined
|
||||
) {
|
||||
toggle.checked = settings.enabledPlatforms[platform];
|
||||
}
|
||||
});
|
||||
|
||||
// 如果缺少域名则显示状态
|
||||
if (!domainValue && settings.enabled) {
|
||||
@@ -52,9 +105,144 @@ function setupEventListeners() {
|
||||
.addEventListener("blur", validateDomain);
|
||||
|
||||
// 平台切换
|
||||
document.getElementById("ghToggle").addEventListener("change", saveSettings);
|
||||
document.getElementById("glToggle").addEventListener("change", saveSettings);
|
||||
document.getElementById("hfToggle").addEventListener("change", saveSettings);
|
||||
const platformToggles = [
|
||||
"ghToggle",
|
||||
"glToggle",
|
||||
"giteaToggle",
|
||||
"codebergToggle",
|
||||
"sfToggle",
|
||||
"aospToggle",
|
||||
"hfToggle",
|
||||
"npmToggle",
|
||||
"pypiToggle",
|
||||
"condaToggle",
|
||||
"mavenToggle",
|
||||
"rubygemsToggle",
|
||||
"cratesToggle",
|
||||
"nugetToggle",
|
||||
"golangToggle",
|
||||
"arxivToggle",
|
||||
"fdroidToggle",
|
||||
];
|
||||
|
||||
platformToggles.forEach((toggleId) => {
|
||||
const element = document.getElementById(toggleId);
|
||||
if (element) {
|
||||
element.addEventListener("change", saveSettings);
|
||||
}
|
||||
});
|
||||
|
||||
// 滚动优化逻辑
|
||||
setupScrollOptimization();
|
||||
}
|
||||
|
||||
function setupScrollOptimization() {
|
||||
const contentElement = document.querySelector(".content");
|
||||
const platformsElement = document.querySelector(".platforms");
|
||||
|
||||
// 为主内容区域添加滚动优化
|
||||
if (contentElement) {
|
||||
let scrollTimeout;
|
||||
|
||||
contentElement.addEventListener("scroll", () => {
|
||||
// 添加滚动中的视觉反馈
|
||||
contentElement.style.scrollbarWidth = "auto";
|
||||
|
||||
clearTimeout(scrollTimeout);
|
||||
scrollTimeout = setTimeout(() => {
|
||||
// 滚动结束后隐藏滚动条(仅在不悬停时)
|
||||
if (!contentElement.matches(":hover")) {
|
||||
contentElement.style.scrollbarWidth = "thin";
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// 鼠标悬停时显示滚动条
|
||||
contentElement.addEventListener("mouseenter", () => {
|
||||
contentElement.style.scrollbarWidth = "auto";
|
||||
});
|
||||
|
||||
contentElement.addEventListener("mouseleave", () => {
|
||||
contentElement.style.scrollbarWidth = "thin";
|
||||
});
|
||||
}
|
||||
|
||||
// 为平台列表添加滚动优化
|
||||
if (platformsElement) {
|
||||
let scrollTimeout;
|
||||
|
||||
platformsElement.addEventListener("scroll", () => {
|
||||
platformsElement.style.scrollbarWidth = "auto";
|
||||
|
||||
clearTimeout(scrollTimeout);
|
||||
scrollTimeout = setTimeout(() => {
|
||||
if (!platformsElement.matches(":hover")) {
|
||||
platformsElement.style.scrollbarWidth = "thin";
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
platformsElement.addEventListener("mouseenter", () => {
|
||||
platformsElement.style.scrollbarWidth = "auto";
|
||||
});
|
||||
|
||||
platformsElement.addEventListener("mouseleave", () => {
|
||||
platformsElement.style.scrollbarWidth = "thin";
|
||||
});
|
||||
}
|
||||
|
||||
// 添加键盘导航支持
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const activeElement = document.activeElement;
|
||||
const isInScrollableArea =
|
||||
activeElement &&
|
||||
(contentElement.contains(activeElement) ||
|
||||
platformsElement.contains(activeElement));
|
||||
|
||||
if (isInScrollableArea) {
|
||||
switch (e.key) {
|
||||
case "ArrowUp":
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
const scrollable = contentElement.contains(activeElement)
|
||||
? contentElement
|
||||
: platformsElement;
|
||||
scrollable.scrollBy({ top: -50, behavior: "smooth" });
|
||||
}
|
||||
break;
|
||||
case "ArrowDown":
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
const scrollable = contentElement.contains(activeElement)
|
||||
? contentElement
|
||||
: platformsElement;
|
||||
scrollable.scrollBy({ top: 50, behavior: "smooth" });
|
||||
}
|
||||
break;
|
||||
case "Home":
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
const scrollable = contentElement.contains(activeElement)
|
||||
? contentElement
|
||||
: platformsElement;
|
||||
scrollable.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
break;
|
||||
case "End":
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
const scrollable = contentElement.contains(activeElement)
|
||||
? contentElement
|
||||
: platformsElement;
|
||||
scrollable.scrollTo({
|
||||
top: scrollable.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
@@ -73,9 +261,38 @@ async function saveSettings() {
|
||||
enabled: document.getElementById("enabledToggle").checked,
|
||||
xgetDomain: domainValue,
|
||||
enabledPlatforms: {
|
||||
gh: document.getElementById("ghToggle").checked,
|
||||
gl: document.getElementById("glToggle").checked,
|
||||
hf: document.getElementById("hfToggle").checked,
|
||||
// 代码托管平台
|
||||
gh: document.getElementById("ghToggle")?.checked || false,
|
||||
gl: document.getElementById("glToggle")?.checked || false,
|
||||
gitea: document.getElementById("giteaToggle")?.checked || false,
|
||||
codeberg: document.getElementById("codebergToggle")?.checked || false,
|
||||
sf: document.getElementById("sfToggle")?.checked || false,
|
||||
aosp: document.getElementById("aospToggle")?.checked || false,
|
||||
|
||||
// AI/ML 平台
|
||||
hf: document.getElementById("hfToggle")?.checked || false,
|
||||
|
||||
// 包管理平台
|
||||
npm: document.getElementById("npmToggle")?.checked || false,
|
||||
pypi: document.getElementById("pypiToggle")?.checked || false,
|
||||
"pypi-files": true, // 默认开启
|
||||
conda: document.getElementById("condaToggle")?.checked || false,
|
||||
"conda-community": true, // 默认开启
|
||||
maven: document.getElementById("mavenToggle")?.checked || false,
|
||||
apache: true, // 默认开启
|
||||
gradle: true, // 默认开启
|
||||
rubygems: document.getElementById("rubygemsToggle")?.checked || false,
|
||||
cran: true, // 默认开启
|
||||
cpan: true, // 默认开启
|
||||
ctan: true, // 默认开启
|
||||
golang: document.getElementById("golangToggle")?.checked || false,
|
||||
nuget: document.getElementById("nugetToggle")?.checked || false,
|
||||
crates: document.getElementById("cratesToggle")?.checked || false,
|
||||
packagist: true, // 默认开启
|
||||
|
||||
// 其他平台
|
||||
arxiv: document.getElementById("arxivToggle")?.checked || false,
|
||||
fdroid: document.getElementById("fdroidToggle")?.checked || false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -84,7 +301,7 @@ async function saveSettings() {
|
||||
settings.xgetDomain = cleanupDomain(settings.xgetDomain);
|
||||
}
|
||||
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
const response = await webext.runtime.sendMessage({
|
||||
action: "saveSettings",
|
||||
settings: settings,
|
||||
});
|
||||
@@ -147,6 +364,7 @@ function showStatus(message, type) {
|
||||
statusElement.textContent = message;
|
||||
statusElement.className = `status ${type}`;
|
||||
statusElement.style.display = "block";
|
||||
statusElement.classList.remove("hiding");
|
||||
|
||||
// 自动隐藏成功消息
|
||||
if (type === "success") {
|
||||
@@ -156,7 +374,15 @@ function showStatus(message, type) {
|
||||
|
||||
function hideStatus() {
|
||||
const statusElement = document.getElementById("status");
|
||||
statusElement.style.display = "none";
|
||||
statusElement.classList.add("hiding");
|
||||
statusElement.addEventListener(
|
||||
"animationend",
|
||||
() => {
|
||||
statusElement.style.display = "none";
|
||||
statusElement.classList.remove("hiding");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
/**
|
||||
* WebExtension API 兼容层
|
||||
*
|
||||
* 提供跨浏览器的统一 API 接口,解决不同浏览器间的差异:
|
||||
* - Firefox 使用 browser API,返回 Promise
|
||||
* - Chromium 使用 chrome API,使用回调函数
|
||||
* - 不同版本的 Manifest API 差异 (V2 vs V3)
|
||||
*
|
||||
* 功能:
|
||||
* - 自动检测当前浏览器环境
|
||||
* - 提供统一的 Promise 化 API 接口
|
||||
* - 处理 API 调用失败的情况
|
||||
* - 兼容不同浏览器的特性差异
|
||||
*/
|
||||
|
||||
/**
|
||||
* 检测当前浏览器环境
|
||||
*
|
||||
* @returns {Object} 浏览器特性信息
|
||||
* @property {number} major - 浏览器主版本号
|
||||
* @property {Set<string>} soup - 浏览器特性标识集合
|
||||
*/
|
||||
const detectBrowser = () => {
|
||||
const ua = navigator.userAgent;
|
||||
let manifest;
|
||||
|
||||
try {
|
||||
manifest = chrome.runtime.getManifest();
|
||||
} catch (error) {
|
||||
console.warn("Could not get manifest in current context:", error);
|
||||
manifest = { version: "0.0.0" }; // 默认版本
|
||||
}
|
||||
|
||||
const flavor = {
|
||||
major: 0,
|
||||
soup: new Set(["webext"]),
|
||||
};
|
||||
|
||||
// 检测 Firefox
|
||||
if (
|
||||
typeof browser !== "undefined" &&
|
||||
browser.runtime &&
|
||||
browser.runtime.getURL("").startsWith("moz-extension://")
|
||||
) {
|
||||
flavor.soup.add("firefox");
|
||||
const match = /Firefox\/(\d+)/.exec(ua);
|
||||
flavor.major = match ? parseInt(match[1], 10) : 115;
|
||||
|
||||
// Firefox 特性检测
|
||||
if (CSS.supports("selector(a:has(b))")) {
|
||||
flavor.soup.add("native_css_has");
|
||||
}
|
||||
}
|
||||
// 检测基于 Chromium 的浏览器
|
||||
else {
|
||||
const match = /\bChrom(?:e|ium)\/(\d+)/.exec(ua);
|
||||
if (match !== null) {
|
||||
flavor.soup.add("chromium");
|
||||
}
|
||||
flavor.major = match ? parseInt(match[1], 10) : 120;
|
||||
|
||||
// 检测具体浏览器
|
||||
if (/\bEdg\/\b/.test(ua)) {
|
||||
flavor.soup.add("edge");
|
||||
} else if (/\bOPR\/\b/.test(ua)) {
|
||||
flavor.soup.add("opera");
|
||||
} else {
|
||||
flavor.soup.add("chrome");
|
||||
}
|
||||
}
|
||||
|
||||
// 检测移动端
|
||||
if (/\bMobile\b/.test(ua)) {
|
||||
flavor.soup.add("mobile");
|
||||
}
|
||||
|
||||
// 检测开发版本
|
||||
if (/^\d+\.\d+\.\d+\D/.test(manifest.version)) {
|
||||
flavor.soup.add("devbuild");
|
||||
}
|
||||
|
||||
return flavor;
|
||||
};
|
||||
|
||||
// 浏览器特性检测
|
||||
const webextFlavor = detectBrowser();
|
||||
|
||||
// 统一的 WebExtension API
|
||||
const webext = (() => {
|
||||
// Firefox 使用 browser API,Chrome 使用 chrome API
|
||||
let api;
|
||||
try {
|
||||
api = typeof browser !== "undefined" && browser.runtime ? browser : chrome;
|
||||
if (!api || !api.runtime) {
|
||||
throw new Error("No WebExtension API available");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize WebExtension API:", error);
|
||||
// 创建一个最小的 API 存根
|
||||
api = {
|
||||
runtime: { getManifest: () => ({ version: "0.0.0" }) },
|
||||
storage: { local: {}, sync: {} },
|
||||
tabs: {},
|
||||
downloads: {}
|
||||
};
|
||||
}
|
||||
|
||||
// Promise 化函数的包装器
|
||||
const promisify = (context, methodName) => {
|
||||
const method = context[methodName];
|
||||
if (!method) {
|
||||
throw new Error(`Method ${methodName} not found`);
|
||||
}
|
||||
|
||||
return function (...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
method.call(context, ...args, (result) => {
|
||||
if (api.runtime.lastError) {
|
||||
reject(new Error(api.runtime.lastError.message));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// 无失败的 Promise 包装器
|
||||
const promisifyNoFail = (context, methodName, defaultValue = undefined) => {
|
||||
try {
|
||||
const promisified = promisify(context, methodName);
|
||||
return async function (...args) {
|
||||
try {
|
||||
return await promisified(...args);
|
||||
} catch (error) {
|
||||
console.warn(`WebExt API call failed: ${methodName}`, error);
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// 如果方法不存在,返回空函数
|
||||
return async function () {
|
||||
console.warn(`WebExt API method not available: ${methodName}`);
|
||||
return defaultValue;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 专门的 sendMessage 包装器
|
||||
const createSendMessage = (runtimeAPI) => {
|
||||
if (!runtimeAPI || !runtimeAPI.sendMessage) {
|
||||
return async () => {
|
||||
console.warn("runtime.sendMessage not available");
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
return function (...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (args.length === 1) {
|
||||
// sendMessage(message)
|
||||
runtimeAPI.sendMessage(args[0], (response) => {
|
||||
if (api.runtime.lastError) {
|
||||
console.warn("SendMessage failed:", api.runtime.lastError.message);
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
} else if (args.length === 2) {
|
||||
// 检查第二个参数是否是函数(回调)
|
||||
if (typeof args[1] === 'function') {
|
||||
// sendMessage(message, callback) - 直接调用回调,不使用 Promise
|
||||
runtimeAPI.sendMessage(args[0], args[1]);
|
||||
resolve(undefined); // 立即解析 Promise
|
||||
} else if (typeof args[0] === 'string' && args[0].length > 10) {
|
||||
// sendMessage(extensionId, message)
|
||||
runtimeAPI.sendMessage(args[0], args[1], (response) => {
|
||||
if (api.runtime.lastError) {
|
||||
console.warn("SendMessage failed:", api.runtime.lastError.message);
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// sendMessage(message, options)
|
||||
runtimeAPI.sendMessage(args[0], args[1], (response) => {
|
||||
if (api.runtime.lastError) {
|
||||
console.warn("SendMessage failed:", api.runtime.lastError.message);
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (args.length === 3) {
|
||||
// 检查第三个参数是否是函数(回调)
|
||||
if (typeof args[2] === 'function') {
|
||||
// sendMessage(extensionId, message, callback) 或 sendMessage(message, options, callback)
|
||||
runtimeAPI.sendMessage(args[0], args[1], args[2]);
|
||||
resolve(undefined); // 立即解析 Promise
|
||||
} else {
|
||||
// sendMessage(extensionId, message, options)
|
||||
runtimeAPI.sendMessage(args[0], args[1], args[2], (response) => {
|
||||
if (api.runtime.lastError) {
|
||||
console.warn("SendMessage failed:", api.runtime.lastError.message);
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (args.length === 4 && typeof args[3] === 'function') {
|
||||
// sendMessage(extensionId, message, options, callback)
|
||||
runtimeAPI.sendMessage(args[0], args[1], args[2], args[3]);
|
||||
resolve(undefined); // 立即解析 Promise
|
||||
} else {
|
||||
console.warn("Invalid arguments for sendMessage:", args);
|
||||
resolve(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("SendMessage error:", error);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// 专门的 tabs.sendMessage 包装器
|
||||
const createTabsSendMessage = (tabsAPI) => {
|
||||
if (!tabsAPI || !tabsAPI.sendMessage) {
|
||||
return async () => {
|
||||
console.warn("tabs.sendMessage not available");
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
return function (tabId, message, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (arguments.length === 2) {
|
||||
// sendMessage(tabId, message)
|
||||
tabsAPI.sendMessage(tabId, message, (response) => {
|
||||
if (api.runtime.lastError) {
|
||||
console.warn("Tabs sendMessage failed:", api.runtime.lastError.message);
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// sendMessage(tabId, message, options)
|
||||
tabsAPI.sendMessage(tabId, message, options, (response) => {
|
||||
if (api.runtime.lastError) {
|
||||
console.warn("Tabs sendMessage failed:", api.runtime.lastError.message);
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Tabs sendMessage error:", error);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
// Runtime API
|
||||
runtime: {
|
||||
getManifest: () => api.runtime.getManifest(),
|
||||
getURL: (path) => api.runtime.getURL(path),
|
||||
sendMessage: createSendMessage(api.runtime),
|
||||
connect: (connectInfo) => api.runtime.connect(connectInfo),
|
||||
onMessage: api.runtime.onMessage,
|
||||
onConnect: api.runtime.onConnect,
|
||||
onInstalled: api.runtime.onInstalled,
|
||||
onStartup: api.runtime.onStartup,
|
||||
lastError: api.runtime.lastError,
|
||||
id: api.runtime.id,
|
||||
},
|
||||
|
||||
// Tabs API
|
||||
tabs: api.tabs ? {
|
||||
query: promisifyNoFail(api.tabs, "query", []),
|
||||
get: promisifyNoFail(api.tabs, "get"),
|
||||
create: promisifyNoFail(api.tabs, "create"),
|
||||
update: promisifyNoFail(api.tabs, "update"),
|
||||
remove: promisifyNoFail(api.tabs, "remove"),
|
||||
sendMessage: createTabsSendMessage(api.tabs),
|
||||
executeScript: promisifyNoFail(api.tabs, "executeScript"),
|
||||
insertCSS: promisifyNoFail(api.tabs, "insertCSS"),
|
||||
removeCSS: promisifyNoFail(api.tabs, "removeCSS"),
|
||||
onUpdated: api.tabs.onUpdated,
|
||||
onActivated: api.tabs.onActivated,
|
||||
onRemoved: api.tabs.onRemoved,
|
||||
} : {
|
||||
query: async () => [],
|
||||
get: async () => null,
|
||||
create: async () => null,
|
||||
update: async () => null,
|
||||
remove: async () => null,
|
||||
sendMessage: async () => null,
|
||||
executeScript: async () => null,
|
||||
insertCSS: async () => null,
|
||||
removeCSS: async () => null,
|
||||
onUpdated: { addListener: () => {}, removeListener: () => {} },
|
||||
onActivated: { addListener: () => {}, removeListener: () => {} },
|
||||
onRemoved: { addListener: () => {}, removeListener: () => {} },
|
||||
},
|
||||
|
||||
// Storage API
|
||||
storage: {
|
||||
local: {
|
||||
get: promisifyNoFail(api.storage.local, "get", {}),
|
||||
set: promisifyNoFail(api.storage.local, "set"),
|
||||
remove: promisifyNoFail(api.storage.local, "remove"),
|
||||
clear: promisifyNoFail(api.storage.local, "clear"),
|
||||
getBytesInUse: promisifyNoFail(api.storage.local, "getBytesInUse", 0),
|
||||
},
|
||||
sync: api.storage.sync
|
||||
? {
|
||||
get: promisifyNoFail(api.storage.sync, "get", {}),
|
||||
set: promisifyNoFail(api.storage.sync, "set"),
|
||||
remove: promisifyNoFail(api.storage.sync, "remove"),
|
||||
clear: promisifyNoFail(api.storage.sync, "clear"),
|
||||
getBytesInUse: promisifyNoFail(
|
||||
api.storage.sync,
|
||||
"getBytesInUse",
|
||||
0
|
||||
),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
|
||||
// Downloads API
|
||||
downloads: api.downloads ? {
|
||||
download: promisifyNoFail(api.downloads, "download"),
|
||||
search: promisifyNoFail(api.downloads, "search", []),
|
||||
cancel: promisifyNoFail(api.downloads, "cancel"),
|
||||
pause: promisifyNoFail(api.downloads, "pause"),
|
||||
resume: promisifyNoFail(api.downloads, "resume"),
|
||||
erase: promisifyNoFail(api.downloads, "erase"),
|
||||
removeFile: promisifyNoFail(api.downloads, "removeFile"),
|
||||
show: promisifyNoFail(api.downloads, "show"),
|
||||
showDefaultFolder: promisifyNoFail(api.downloads, "showDefaultFolder"),
|
||||
onCreated: api.downloads.onCreated,
|
||||
onChanged: api.downloads.onChanged,
|
||||
onErased: api.downloads.onErased,
|
||||
onDeterminingFilename: api.downloads.onDeterminingFilename,
|
||||
} : {
|
||||
download: async () => null,
|
||||
search: async () => [],
|
||||
cancel: async () => null,
|
||||
pause: async () => null,
|
||||
resume: async () => null,
|
||||
erase: async () => null,
|
||||
removeFile: async () => null,
|
||||
show: async () => null,
|
||||
showDefaultFolder: async () => null,
|
||||
onCreated: { addListener: () => {}, removeListener: () => {} },
|
||||
onChanged: { addListener: () => {}, removeListener: () => {} },
|
||||
onErased: { addListener: () => {}, removeListener: () => {} },
|
||||
onDeterminingFilename: { addListener: () => {}, removeListener: () => {} },
|
||||
},
|
||||
|
||||
// Browser Action / Action API (兼容不同版本)
|
||||
action: (() => {
|
||||
// Manifest V3 使用 action,V2 使用 browserAction
|
||||
const actionAPI = api.action || api.browserAction;
|
||||
if (!actionAPI) return null;
|
||||
|
||||
return {
|
||||
setTitle: promisifyNoFail(actionAPI, "setTitle"),
|
||||
getTitle: promisifyNoFail(actionAPI, "getTitle"),
|
||||
setIcon: promisifyNoFail(actionAPI, "setIcon"),
|
||||
setPopup: promisifyNoFail(actionAPI, "setPopup"),
|
||||
getPopup: promisifyNoFail(actionAPI, "getPopup"),
|
||||
setBadgeText: promisifyNoFail(actionAPI, "setBadgeText"),
|
||||
getBadgeText: promisifyNoFail(actionAPI, "getBadgeText"),
|
||||
setBadgeBackgroundColor: promisifyNoFail(
|
||||
actionAPI,
|
||||
"setBadgeBackgroundColor"
|
||||
),
|
||||
getBadgeBackgroundColor: promisifyNoFail(
|
||||
actionAPI,
|
||||
"getBadgeBackgroundColor"
|
||||
),
|
||||
setBadgeTextColor: promisifyNoFail(actionAPI, "setBadgeTextColor"),
|
||||
getBadgeTextColor: promisifyNoFail(actionAPI, "getBadgeTextColor"),
|
||||
enable: promisifyNoFail(actionAPI, "enable"),
|
||||
disable: promisifyNoFail(actionAPI, "disable"),
|
||||
onClicked: actionAPI.onClicked,
|
||||
};
|
||||
})(),
|
||||
|
||||
// Permissions API
|
||||
permissions: {
|
||||
contains: promisifyNoFail(api.permissions, "contains", false),
|
||||
getAll: promisifyNoFail(api.permissions, "getAll", {
|
||||
origins: [],
|
||||
permissions: [],
|
||||
}),
|
||||
request: promisifyNoFail(api.permissions, "request", false),
|
||||
remove: promisifyNoFail(api.permissions, "remove", false),
|
||||
onAdded: api.permissions?.onAdded,
|
||||
onRemoved: api.permissions?.onRemoved,
|
||||
},
|
||||
|
||||
// i18n API
|
||||
i18n: {
|
||||
getMessage: (messageName, substitutions) =>
|
||||
api.i18n.getMessage(messageName, substitutions),
|
||||
getAcceptLanguages: promisifyNoFail(api.i18n, "getAcceptLanguages", []),
|
||||
getUILanguage: () => api.i18n.getUILanguage(),
|
||||
detectLanguage: promisifyNoFail(api.i18n, "detectLanguage"),
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
// 导出接口
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
// Node.js 环境
|
||||
module.exports = { webext, webextFlavor };
|
||||
} else if (typeof window !== "undefined") {
|
||||
// 浏览器环境(内容脚本、弹出窗口等)
|
||||
window.webext = webext;
|
||||
window.webextFlavor = webextFlavor;
|
||||
console.log("WebExt compatibility layer loaded in browser environment");
|
||||
} else if (typeof self !== "undefined") {
|
||||
// 服务工作器环境
|
||||
self.webext = webext;
|
||||
self.webextFlavor = webextFlavor;
|
||||
console.log("WebExt compatibility layer loaded in service worker environment");
|
||||
} else {
|
||||
// 全局环境
|
||||
this.webext = webext;
|
||||
this.webextFlavor = webextFlavor;
|
||||
console.log("WebExt compatibility layer loaded in global environment");
|
||||
}
|
||||
Reference in new issue
Block a user