mirror of
https://github.com/AmintaCCCP/GithubStarsManager.git
synced 2025-11-25 02:34:54 +08:00
0.1.3
This commit is contained in:
148
UPDATE_FEATURE_GUIDE.md
Normal file
148
UPDATE_FEATURE_GUIDE.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# 检查更新功能实现指南
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
已成功为 GitHub Stars Manager 添加了完整的检查更新功能,包括:
|
||||||
|
|
||||||
|
1. **版本信息管理** - 使用XML格式存储版本信息
|
||||||
|
2. **自动更新检查** - 应用启动时自动检查更新
|
||||||
|
3. **手动更新检查** - 设置页面中的检查更新按钮
|
||||||
|
4. **更新提示界面** - 美观的更新对话框
|
||||||
|
5. **版本管理工具** - 自动化版本更新脚本
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
├── versions/
|
||||||
|
│ ├── version-info.xml # 版本信息XML文件
|
||||||
|
│ └── README.md # 版本管理说明
|
||||||
|
├── src/
|
||||||
|
│ ├── services/
|
||||||
|
│ │ └── updateService.ts # 更新检查服务
|
||||||
|
│ └── components/
|
||||||
|
│ └── UpdateChecker.tsx # 更新检查组件
|
||||||
|
├── scripts/
|
||||||
|
│ └── update-version.js # 版本更新脚本
|
||||||
|
└── test-update.html # 功能测试页面
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 发布新版本
|
||||||
|
|
||||||
|
使用自动化脚本更新版本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run update-version 0.1.4 "新增功能A" "修复bug B" "优化性能C"
|
||||||
|
```
|
||||||
|
|
||||||
|
这个命令会自动:
|
||||||
|
- 更新 `package.json` 中的版本号
|
||||||
|
- 在 `versions/version-info.xml` 中添加新版本记录
|
||||||
|
- 更新 `src/services/updateService.ts` 中的当前版本号
|
||||||
|
|
||||||
|
### 2. 提交到仓库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "chore: bump version to v0.1.4"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 创建GitHub Release
|
||||||
|
|
||||||
|
1. 在GitHub仓库中创建新的Release
|
||||||
|
2. 标签名称:`v0.1.4`
|
||||||
|
3. 上传构建好的安装包(如 `github-stars-manager-0.1.4.dmg`)
|
||||||
|
4. 确保下载链接与XML中的URL一致
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 自动检查更新
|
||||||
|
- 应用启动3秒后自动检查更新
|
||||||
|
- 静默检查,不影响用户体验
|
||||||
|
- 发现新版本时在控制台记录日志
|
||||||
|
|
||||||
|
### 手动检查更新
|
||||||
|
- 设置页面中的"检查更新"按钮
|
||||||
|
- 实时显示检查状态
|
||||||
|
- 显示详细的更新信息
|
||||||
|
|
||||||
|
### 更新提示界面
|
||||||
|
- 美观的模态对话框
|
||||||
|
- 显示版本号和发布日期
|
||||||
|
- 详细的更新日志列表
|
||||||
|
- 一键跳转到下载页面
|
||||||
|
|
||||||
|
### 版本比较算法
|
||||||
|
- 支持语义化版本号(x.y.z)
|
||||||
|
- 智能比较版本大小
|
||||||
|
- 处理不同长度的版本号
|
||||||
|
|
||||||
|
## XML文件格式
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<versions>
|
||||||
|
<version>
|
||||||
|
<number>0.1.3</number>
|
||||||
|
<releaseDate>2025-01-04</releaseDate>
|
||||||
|
<changelog>
|
||||||
|
<item>添加检查更新功能</item>
|
||||||
|
<item>优化用户界面</item>
|
||||||
|
<item>修复已知bug</item>
|
||||||
|
</changelog>
|
||||||
|
<downloadUrl>https://github.com/AmintaCCCP/GithubStarsManager/releases/download/v0.1.3/github-stars-manager-0.1.3.dmg</downloadUrl>
|
||||||
|
</version>
|
||||||
|
</versions>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试方法
|
||||||
|
|
||||||
|
### 本地测试
|
||||||
|
1. 打开 `test-update.html` 文件
|
||||||
|
2. 点击"检查更新"按钮
|
||||||
|
3. 验证功能是否正常工作
|
||||||
|
|
||||||
|
### 应用内测试
|
||||||
|
1. 启动应用,等待3秒观察控制台日志
|
||||||
|
2. 进入设置页面,点击"检查更新"
|
||||||
|
3. 验证更新对话框是否正确显示
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **版本号格式**:必须使用 x.y.z 格式的语义化版本号
|
||||||
|
2. **XML文件编码**:确保使用UTF-8编码
|
||||||
|
3. **下载链接**:确保GitHub Release中的下载链接可用
|
||||||
|
4. **网络请求**:更新检查需要网络连接
|
||||||
|
5. **CORS问题**:本地测试时可能遇到跨域问题
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
- 网络连接失败时显示友好错误信息
|
||||||
|
- XML解析错误时提供详细错误描述
|
||||||
|
- 版本比较异常时使用默认处理逻辑
|
||||||
|
|
||||||
|
## 多语言支持
|
||||||
|
|
||||||
|
更新功能已集成应用的多语言系统:
|
||||||
|
- 中文界面显示中文提示
|
||||||
|
- 英文界面显示英文提示
|
||||||
|
- 自动根据应用语言设置调整
|
||||||
|
|
||||||
|
## 未来扩展
|
||||||
|
|
||||||
|
可以考虑添加的功能:
|
||||||
|
1. 自动下载更新包
|
||||||
|
2. 增量更新支持
|
||||||
|
3. 更新进度显示
|
||||||
|
4. 更新历史记录
|
||||||
|
5. 跳过版本功能
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
- **前端框架**:React + TypeScript
|
||||||
|
- **HTTP请求**:Fetch API
|
||||||
|
- **XML解析**:DOMParser
|
||||||
|
- **版本比较**:自定义算法
|
||||||
|
- **UI组件**:Tailwind CSS + Lucide Icons
|
||||||
38
package-lock.json
generated
38
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "github-stars-manager",
|
"name": "github-stars-manager",
|
||||||
"version": "1.0.0",
|
"version": "0.1.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "github-stars-manager",
|
"name": "github-stars-manager",
|
||||||
"version": "1.0.0",
|
"version": "0.1.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
@@ -29,7 +29,8 @@
|
|||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.3.0",
|
"typescript-eslint": "^8.3.0",
|
||||||
"vite": "^5.4.2"
|
"vite": "^5.4.2",
|
||||||
|
"xml2js": "^0.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@@ -3449,6 +3450,13 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sax": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.23.2",
|
"version": "0.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
@@ -4071,6 +4079,30 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xml2js": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sax": ">=0.6.0",
|
||||||
|
"xmlbuilder": "~11.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlbuilder": {
|
||||||
|
"version": "11.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||||
|
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "github-stars-manager",
|
"name": "github-stars-manager",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -11,15 +11,16 @@
|
|||||||
"build:desktop": "node scripts/build-desktop.js",
|
"build:desktop": "node scripts/build-desktop.js",
|
||||||
"electron": "electron electron/main.js",
|
"electron": "electron electron/main.js",
|
||||||
"electron:dev": "NODE_ENV=development electron electron/main.js",
|
"electron:dev": "NODE_ENV=development electron electron/main.js",
|
||||||
"dist": "electron-builder"
|
"dist": "electron-builder",
|
||||||
|
"update-version": "node scripts/update-version.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"date-fns": "^3.3.1",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.22.0",
|
"react-router-dom": "^6.22.0",
|
||||||
"zustand": "^4.5.0",
|
"zustand": "^4.5.0"
|
||||||
"date-fns": "^3.3.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.1",
|
"@eslint/js": "^9.9.1",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.3.0",
|
"typescript-eslint": "^8.3.0",
|
||||||
"vite": "^5.4.2"
|
"vite": "^5.4.2",
|
||||||
|
"xml2js": "^0.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
231
scripts/update-version.cjs
Normal file
231
scripts/update-version.cjs
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新版本信息的脚本
|
||||||
|
* 使用方法:
|
||||||
|
* node scripts/update-version.cjs [version] [changelog...] [--url=downloadUrl]
|
||||||
|
* node scripts/update-version.cjs --list (列出所有版本)
|
||||||
|
* node scripts/update-version.cjs --current (显示当前版本)
|
||||||
|
*
|
||||||
|
* 例如:
|
||||||
|
* node scripts/update-version.cjs 0.1.3 "修复搜索bug" "添加新功能"
|
||||||
|
* node scripts/update-version.cjs 0.1.3 "修复bug" --url="https://github.com/AmintaCCCP/GithubStarsManager/releases/tag/v0.1.3-fix"
|
||||||
|
*/
|
||||||
|
|
||||||
|
function updateVersionInfo() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
// 处理特殊命令
|
||||||
|
if (args.length === 1) {
|
||||||
|
if (args[0] === '--list') {
|
||||||
|
listVersions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args[0] === '--current') {
|
||||||
|
showCurrentVersion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args[0] === '--help' || args[0] === '-h') {
|
||||||
|
showHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length < 2) {
|
||||||
|
console.error('❌ 参数不足');
|
||||||
|
showHelp();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVersion = args[0];
|
||||||
|
|
||||||
|
// 解析参数,查找自定义下载链接
|
||||||
|
let customDownloadUrl = null;
|
||||||
|
const changelog = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
if (arg.startsWith('--url=')) {
|
||||||
|
customDownloadUrl = arg.substring(6);
|
||||||
|
} else {
|
||||||
|
changelog.push(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证版本号格式
|
||||||
|
if (!/^\d+\.\d+\.\d+$/.test(newVersion)) {
|
||||||
|
console.error('❌ 版本号格式错误,应该是 x.y.z 格式');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证至少有一条更新日志
|
||||||
|
if (changelog.length === 0) {
|
||||||
|
console.error('❌ 至少需要提供一条更新日志');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新 package.json
|
||||||
|
updatePackageJson(newVersion);
|
||||||
|
|
||||||
|
// 更新 version-info.xml
|
||||||
|
updateVersionXML(newVersion, changelog, customDownloadUrl);
|
||||||
|
|
||||||
|
// 更新 UpdateService 中的版本号
|
||||||
|
updateServiceVersion(newVersion);
|
||||||
|
|
||||||
|
console.log(`✅ 版本已更新到 ${newVersion}`);
|
||||||
|
console.log('📝 更新内容:');
|
||||||
|
changelog.forEach((item, index) => {
|
||||||
|
console.log(` ${index + 1}. ${item}`);
|
||||||
|
});
|
||||||
|
if (customDownloadUrl) {
|
||||||
|
console.log(`🔗 自定义下载链接: ${customDownloadUrl}`);
|
||||||
|
}
|
||||||
|
console.log('\n🔄 请记得提交这些更改到 Git 仓库');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 更新版本失败:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePackageJson(version) {
|
||||||
|
const packagePath = path.join(__dirname, '../package.json');
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||||
|
|
||||||
|
packageJson.version = version;
|
||||||
|
|
||||||
|
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||||
|
console.log(`📦 已更新 package.json 版本到 ${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVersionXML(version, changelog, customDownloadUrl) {
|
||||||
|
const xmlPath = path.join(__dirname, '../versions/version-info.xml');
|
||||||
|
const currentDate = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
let xmlContent;
|
||||||
|
try {
|
||||||
|
xmlContent = fs.readFileSync(xmlPath, 'utf8');
|
||||||
|
} catch (error) {
|
||||||
|
// 如果文件不存在,创建新的XML文件
|
||||||
|
xmlContent = '<?xml version="1.0" encoding="UTF-8"?>\n<versions>\n</versions>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成下载链接
|
||||||
|
const downloadUrl = customDownloadUrl ||
|
||||||
|
`https://github.com/AmintaCCCP/GithubStarsManager/releases/download/v${version}/github-stars-manager-${version}.dmg`;
|
||||||
|
|
||||||
|
// 解析现有的XML
|
||||||
|
const versionEntry = ` <version>
|
||||||
|
<number>${version}</number>
|
||||||
|
<releaseDate>${currentDate}</releaseDate>
|
||||||
|
<changelog>
|
||||||
|
${changelog.map(item => ` <item>${escapeXml(item)}</item>`).join('\n')}
|
||||||
|
</changelog>
|
||||||
|
<downloadUrl>${escapeXml(downloadUrl)}</downloadUrl>
|
||||||
|
</version>`;
|
||||||
|
|
||||||
|
// 在 </versions> 前插入新版本
|
||||||
|
const updatedXml = xmlContent.replace('</versions>', `${versionEntry}\n</versions>`);
|
||||||
|
|
||||||
|
fs.writeFileSync(xmlPath, updatedXml);
|
||||||
|
console.log(`📄 已更新 version-info.xml`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateServiceVersion(version) {
|
||||||
|
const servicePath = path.join(__dirname, '../src/services/updateService.ts');
|
||||||
|
let serviceContent = fs.readFileSync(servicePath, 'utf8');
|
||||||
|
|
||||||
|
// 更新版本号
|
||||||
|
serviceContent = serviceContent.replace(
|
||||||
|
/return '\d+\.\d+\.\d+';/,
|
||||||
|
`return '${version}';`
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(servicePath, serviceContent);
|
||||||
|
console.log(`🔧 已更新 UpdateService 版本到 ${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeXml(text) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function listVersions() {
|
||||||
|
const xmlPath = path.join(__dirname, '../versions/version-info.xml');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const xmlContent = fs.readFileSync(xmlPath, 'utf8');
|
||||||
|
const parser = require('xml2js');
|
||||||
|
|
||||||
|
parser.parseString(xmlContent, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('❌ XML解析失败:', err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = result.versions.version || [];
|
||||||
|
console.log('📋 版本历史:');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
versions.forEach((version, index) => {
|
||||||
|
console.log(`${index + 1}. v${version.number[0]} (${version.releaseDate[0]})`);
|
||||||
|
if (version.changelog && version.changelog[0].item) {
|
||||||
|
version.changelog[0].item.forEach(item => {
|
||||||
|
console.log(` • ${item}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 读取版本信息失败:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCurrentVersion() {
|
||||||
|
try {
|
||||||
|
const packagePath = path.join(__dirname, '../package.json');
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||||
|
console.log(`📦 当前版本: v${packageJson.version}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 读取当前版本失败:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHelp() {
|
||||||
|
console.log('📖 版本管理工具使用说明');
|
||||||
|
console.log('');
|
||||||
|
console.log('用法:');
|
||||||
|
console.log(' node scripts/update-version.cjs <version> <changelog...> [--url=downloadUrl]');
|
||||||
|
console.log(' node scripts/update-version.cjs --list 列出所有版本');
|
||||||
|
console.log(' node scripts/update-version.cjs --current 显示当前版本');
|
||||||
|
console.log(' node scripts/update-version.cjs --help 显示帮助');
|
||||||
|
console.log('');
|
||||||
|
console.log('示例:');
|
||||||
|
console.log(' node scripts/update-version.cjs 0.1.3 "修复搜索bug" "添加新功能"');
|
||||||
|
console.log(' node scripts/update-version.cjs 0.1.4 "优化性能" --url="https://github.com/AmintaCCCP/GithubStarsManager/releases/tag/v0.1.4-fix"');
|
||||||
|
console.log(' npm run update-version 0.1.5 "修复已知问题" "提升用户体验"');
|
||||||
|
console.log('');
|
||||||
|
console.log('参数说明:');
|
||||||
|
console.log(' <version> 版本号,格式为 x.y.z');
|
||||||
|
console.log(' <changelog...> 更新日志,至少需要一条');
|
||||||
|
console.log(' --url=<url> 自定义下载链接(可选)');
|
||||||
|
console.log('');
|
||||||
|
console.log('注意:');
|
||||||
|
console.log(' • 版本号必须遵循 x.y.z 格式');
|
||||||
|
console.log(' • 更新日志至少需要一条');
|
||||||
|
console.log(' • 如果不指定 --url,将使用默认的 GitHub Release 链接格式');
|
||||||
|
console.log(' • 更新后记得提交到Git仓库');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行脚本
|
||||||
|
updateVersionInfo();
|
||||||
@@ -7,6 +7,8 @@ import { CategorySidebar } from './components/CategorySidebar';
|
|||||||
import { ReleaseTimeline } from './components/ReleaseTimeline';
|
import { ReleaseTimeline } from './components/ReleaseTimeline';
|
||||||
import { SettingsPanel } from './components/SettingsPanel';
|
import { SettingsPanel } from './components/SettingsPanel';
|
||||||
import { useAppStore } from './store/useAppStore';
|
import { useAppStore } from './store/useAppStore';
|
||||||
|
import { useAutoUpdateCheck } from './components/UpdateChecker';
|
||||||
|
import { UpdateNotificationBanner } from './components/UpdateNotificationBanner';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const {
|
const {
|
||||||
@@ -19,6 +21,9 @@ function App() {
|
|||||||
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||||
|
|
||||||
|
// 自动检查更新
|
||||||
|
useAutoUpdateCheck();
|
||||||
|
|
||||||
// Apply theme to document
|
// Apply theme to document
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (theme === 'dark') {
|
if (theme === 'dark') {
|
||||||
@@ -64,6 +69,7 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
<UpdateNotificationBanner />
|
||||||
<Header />
|
<Header />
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{renderCurrentView()}
|
{renderCurrentView()}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Star, Settings, Calendar, Search, Moon, Sun, LogOut, RefreshCw, Github } from 'lucide-react';
|
import { Star, Settings, Calendar, Search, Moon, Sun, LogOut, RefreshCw } from 'lucide-react';
|
||||||
import { useAppStore } from '../store/useAppStore';
|
import { useAppStore } from '../store/useAppStore';
|
||||||
import { GitHubApiService } from '../services/githubApi';
|
import { GitHubApiService } from '../services/githubApi';
|
||||||
|
|
||||||
@@ -159,17 +159,6 @@ export const Header: React.FC = () => {
|
|||||||
|
|
||||||
{/* User Actions */}
|
{/* User Actions */}
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{/* GitHub Repository Link */}
|
|
||||||
<a
|
|
||||||
href="https://github.com/AmintaCCCP/GithubStarsManager"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
title={t('查看项目源码', 'View project source code')}
|
|
||||||
>
|
|
||||||
<Github className="w-5 h-5 text-gray-700 dark:text-gray-300" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Sync Status */}
|
{/* Sync Status */}
|
||||||
<div className="hidden sm:flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
<div className="hidden sm:flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<span>{t('上次同步:', 'Last sync:')} {formatLastSync(lastSync)}</span>
|
<span>{t('上次同步:', 'Last sync:')} {formatLastSync(lastSync)}</span>
|
||||||
|
|||||||
@@ -14,12 +14,18 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Globe,
|
Globe,
|
||||||
MessageSquare
|
MessageSquare,
|
||||||
|
Package,
|
||||||
|
ExternalLink,
|
||||||
|
Mail,
|
||||||
|
Github,
|
||||||
|
Twitter
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { AIConfig, WebDAVConfig } from '../types';
|
import { AIConfig, WebDAVConfig } from '../types';
|
||||||
import { useAppStore } from '../store/useAppStore';
|
import { useAppStore } from '../store/useAppStore';
|
||||||
import { AIService } from '../services/aiService';
|
import { AIService } from '../services/aiService';
|
||||||
import { WebDAVService } from '../services/webdavService';
|
import { WebDAVService } from '../services/webdavService';
|
||||||
|
import { UpdateChecker } from './UpdateChecker';
|
||||||
|
|
||||||
export const SettingsPanel: React.FC = () => {
|
export const SettingsPanel: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -351,6 +357,28 @@ Focus on practicality and accurate categorization to help users quickly understa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto space-y-8">
|
<div className="max-w-4xl mx-auto space-y-8">
|
||||||
|
{/* Update Check */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||||
|
<div className="flex items-center space-x-3 mb-4">
|
||||||
|
<Package className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{t('检查更新', 'Check for Updates')}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
||||||
|
{t('当前版本: v0.1.3', 'Current Version: v0.1.3')}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-500">
|
||||||
|
{t('检查是否有新版本可用', 'Check if a new version is available')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<UpdateChecker />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Language Settings */}
|
{/* Language Settings */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||||
<div className="flex items-center space-x-3 mb-4">
|
<div className="flex items-center space-x-3 mb-4">
|
||||||
@@ -390,6 +418,42 @@ Focus on practicality and accurate categorization to help users quickly understa
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Information */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||||
|
<div className="flex items-center space-x-3 mb-4">
|
||||||
|
<Mail className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{t('联系方式', 'Contact Information')}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{t('如果您在使用过程中遇到任何问题或有建议,欢迎通过以下方式联系我:', 'If you encounter any issues or have suggestions while using the app, feel free to contact me through:')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => window.open('https://x.com/GoodMan_Lee', '_blank')}
|
||||||
|
className="flex items-center justify-center space-x-2 px-4 py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Twitter className="w-5 h-5" />
|
||||||
|
<span>Twitter</span>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => window.open('https://github.com/AmintaCCCP/GithubStarsManager', '_blank')}
|
||||||
|
className="flex items-center justify-center space-x-2 px-4 py-3 bg-gray-800 hover:bg-gray-900 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Github className="w-5 h-5" />
|
||||||
|
<span>GitHub</span>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* AI Configuration */}
|
{/* AI Configuration */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
|||||||
194
src/components/UpdateChecker.tsx
Normal file
194
src/components/UpdateChecker.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Download, RefreshCw, ExternalLink, Calendar, Package } from 'lucide-react';
|
||||||
|
import { UpdateService, VersionInfo } from '../services/updateService';
|
||||||
|
import { useAppStore } from '../store/useAppStore';
|
||||||
|
|
||||||
|
interface UpdateCheckerProps {
|
||||||
|
onUpdateAvailable?: (version: VersionInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateChecker: React.FC<UpdateCheckerProps> = ({ onUpdateAvailable }) => {
|
||||||
|
const { language, setUpdateNotification } = useAppStore();
|
||||||
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
|
const [updateInfo, setUpdateInfo] = useState<VersionInfo | null>(null);
|
||||||
|
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const t = (zh: string, en: string) => language === 'zh' ? zh : en;
|
||||||
|
|
||||||
|
const checkForUpdates = async (silent = false) => {
|
||||||
|
setIsChecking(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await UpdateService.checkForUpdates();
|
||||||
|
|
||||||
|
if (result.hasUpdate && result.latestVersion) {
|
||||||
|
setUpdateInfo(result.latestVersion);
|
||||||
|
setShowUpdateDialog(true);
|
||||||
|
onUpdateAvailable?.(result.latestVersion);
|
||||||
|
|
||||||
|
// 设置全局更新通知
|
||||||
|
setUpdateNotification({
|
||||||
|
version: result.latestVersion.number,
|
||||||
|
releaseDate: result.latestVersion.releaseDate,
|
||||||
|
changelog: result.latestVersion.changelog,
|
||||||
|
downloadUrl: result.latestVersion.downloadUrl,
|
||||||
|
dismissed: false
|
||||||
|
});
|
||||||
|
} else if (!silent) {
|
||||||
|
// 只在手动检查时显示"已是最新版本"的消息
|
||||||
|
alert(t('当前已是最新版本!', 'You are already using the latest version!'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = t('检查更新失败,请检查网络连接', 'Failed to check for updates. Please check your network connection.');
|
||||||
|
setError(errorMessage);
|
||||||
|
if (!silent) {
|
||||||
|
alert(errorMessage);
|
||||||
|
}
|
||||||
|
console.error('Update check failed:', error);
|
||||||
|
} finally {
|
||||||
|
setIsChecking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (updateInfo?.downloadUrl) {
|
||||||
|
UpdateService.openDownloadUrl(updateInfo.downloadUrl);
|
||||||
|
setShowUpdateDialog(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
try {
|
||||||
|
return new Date(dateString).toLocaleDateString(language === 'zh' ? 'zh-CN' : 'en-US');
|
||||||
|
} catch {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 检查更新按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={() => checkForUpdates(false)}
|
||||||
|
disabled={isChecking}
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isChecking ? (
|
||||||
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{isChecking
|
||||||
|
? t('检查中...', 'Checking...')
|
||||||
|
: t('检查更新', 'Check for Updates')
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{error && (
|
||||||
|
<div className="mt-2 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 更新对话框 */}
|
||||||
|
{showUpdateDialog && updateInfo && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-md w-full max-h-[80vh] overflow-y-auto">
|
||||||
|
<div className="p-6">
|
||||||
|
{/* 标题 */}
|
||||||
|
<div className="flex items-center space-x-3 mb-4">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
|
||||||
|
<Package className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{t('发现新版本', 'New Version Available')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
v{updateInfo.number}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 版本信息 */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center space-x-2 text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
<span>{t('发布日期:', 'Release Date:')} {formatDate(updateInfo.releaseDate)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 更新日志 */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h4 className="font-medium text-gray-900 dark:text-white mb-2">
|
||||||
|
{t('更新内容:', 'What\'s New:')}
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{updateInfo.changelog.map((item, index) => (
|
||||||
|
<li key={index} className="text-sm text-gray-600 dark:text-gray-400 flex items-start space-x-2">
|
||||||
|
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 flex-shrink-0"></span>
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 按钮 */}
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="flex-1 flex items-center justify-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
<span>{t('立即下载', 'Download Now')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowUpdateDialog(false)}
|
||||||
|
className="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
{t('稍后提醒', 'Later')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用于应用启动时自动检查更新的Hook
|
||||||
|
export const useAutoUpdateCheck = () => {
|
||||||
|
const { setUpdateNotification } = useAppStore();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const checkUpdatesOnStartup = async () => {
|
||||||
|
try {
|
||||||
|
const result = await UpdateService.checkForUpdates();
|
||||||
|
if (result.hasUpdate && result.latestVersion) {
|
||||||
|
console.log('New version available:', result.latestVersion.number);
|
||||||
|
|
||||||
|
// 设置全局更新通知
|
||||||
|
setUpdateNotification({
|
||||||
|
version: result.latestVersion.number,
|
||||||
|
releaseDate: result.latestVersion.releaseDate,
|
||||||
|
changelog: result.latestVersion.changelog,
|
||||||
|
downloadUrl: result.latestVersion.downloadUrl,
|
||||||
|
dismissed: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Startup update check failed:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 延迟3秒后检查更新,避免影响应用启动速度
|
||||||
|
const timer = setTimeout(checkUpdatesOnStartup, 3000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [setUpdateNotification]);
|
||||||
|
};
|
||||||
73
src/components/UpdateNotificationBanner.tsx
Normal file
73
src/components/UpdateNotificationBanner.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { X, Download, Calendar, Package } from 'lucide-react';
|
||||||
|
import { useAppStore } from '../store/useAppStore';
|
||||||
|
import { UpdateService } from '../services/updateService';
|
||||||
|
|
||||||
|
export const UpdateNotificationBanner: React.FC = () => {
|
||||||
|
const { updateNotification, dismissUpdateNotification, language } = useAppStore();
|
||||||
|
|
||||||
|
const t = (zh: string, en: string) => language === 'zh' ? zh : en;
|
||||||
|
|
||||||
|
if (!updateNotification || updateNotification.dismissed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
UpdateService.openDownloadUrl(updateNotification.downloadUrl);
|
||||||
|
dismissUpdateNotification();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
try {
|
||||||
|
return new Date(dateString).toLocaleDateString(language === 'zh' ? 'zh-CN' : 'en-US');
|
||||||
|
} catch {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border-b border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
|
||||||
|
<Package className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
||||||
|
{t('发现新版本', 'New Version Available')} v{updateNotification.version}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center space-x-1 text-xs text-blue-700 dark:text-blue-300">
|
||||||
|
<Calendar className="w-3 h-3" />
|
||||||
|
<span>{formatDate(updateNotification.releaseDate)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
||||||
|
{updateNotification.changelog.slice(0, 2).join(' • ')}
|
||||||
|
{updateNotification.changelog.length > 2 && '...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="flex items-center space-x-1 px-3 py-1.5 bg-blue-600 text-white text-xs rounded-md hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Download className="w-3 h-3" />
|
||||||
|
<span>{t('立即下载', 'Download')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={dismissUpdateNotification}
|
||||||
|
className="p-1.5 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-800 rounded-md transition-colors"
|
||||||
|
title={t('关闭', 'Close')}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
119
src/services/updateService.ts
Normal file
119
src/services/updateService.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
export interface VersionInfo {
|
||||||
|
number: string;
|
||||||
|
releaseDate: string;
|
||||||
|
changelog: string[];
|
||||||
|
downloadUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateCheckResult {
|
||||||
|
hasUpdate: boolean;
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion?: VersionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateService {
|
||||||
|
private static readonly REPO_URL = 'https://raw.githubusercontent.com/AmintaCCCP/GithubStarsManager/main/versions/version-info.xml';
|
||||||
|
|
||||||
|
private static getCurrentVersion(): string {
|
||||||
|
// 在实际应用中,这个版本号应该在构建时注入
|
||||||
|
// 这里暂时硬编码,你可以通过构建脚本或环境变量来动态设置
|
||||||
|
return '0.1.3';
|
||||||
|
}
|
||||||
|
|
||||||
|
static async checkForUpdates(): Promise<UpdateCheckResult> {
|
||||||
|
const currentVersion = this.getCurrentVersion();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.REPO_URL);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const xmlText = await response.text();
|
||||||
|
const versions = this.parseVersionXML(xmlText);
|
||||||
|
|
||||||
|
if (versions.length === 0) {
|
||||||
|
return {
|
||||||
|
hasUpdate: false,
|
||||||
|
currentVersion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最新版本(假设XML中版本按时间排序,最后一个是最新的)
|
||||||
|
const latestVersion = versions[versions.length - 1];
|
||||||
|
const hasUpdate = this.compareVersions(currentVersion, latestVersion.number) < 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasUpdate,
|
||||||
|
currentVersion,
|
||||||
|
latestVersion: hasUpdate ? latestVersion : undefined
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查更新失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseVersionXML(xmlText: string): VersionInfo[] {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
||||||
|
|
||||||
|
// 检查解析错误
|
||||||
|
const parseError = xmlDoc.querySelector('parsererror');
|
||||||
|
if (parseError) {
|
||||||
|
throw new Error('XML解析失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions: VersionInfo[] = [];
|
||||||
|
const versionNodes = xmlDoc.querySelectorAll('version');
|
||||||
|
|
||||||
|
versionNodes.forEach(versionNode => {
|
||||||
|
const number = versionNode.querySelector('number')?.textContent?.trim();
|
||||||
|
const releaseDate = versionNode.querySelector('releaseDate')?.textContent?.trim();
|
||||||
|
const downloadUrl = versionNode.querySelector('downloadUrl')?.textContent?.trim();
|
||||||
|
|
||||||
|
if (!number || !releaseDate || !downloadUrl) {
|
||||||
|
return; // 跳过不完整的版本信息
|
||||||
|
}
|
||||||
|
|
||||||
|
const changelog: string[] = [];
|
||||||
|
const changelogItems = versionNode.querySelectorAll('changelog item');
|
||||||
|
changelogItems.forEach(item => {
|
||||||
|
const text = item.textContent?.trim();
|
||||||
|
if (text) {
|
||||||
|
changelog.push(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
versions.push({
|
||||||
|
number,
|
||||||
|
releaseDate,
|
||||||
|
changelog,
|
||||||
|
downloadUrl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static compareVersions(version1: string, version2: string): number {
|
||||||
|
const v1Parts = version1.split('.').map(Number);
|
||||||
|
const v2Parts = version2.split('.').map(Number);
|
||||||
|
|
||||||
|
const maxLength = Math.max(v1Parts.length, v2Parts.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
const v1Part = v1Parts[i] || 0;
|
||||||
|
const v2Part = v2Parts[i] || 0;
|
||||||
|
|
||||||
|
if (v1Part < v2Part) return -1;
|
||||||
|
if (v1Part > v2Part) return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static openDownloadUrl(url: string): void {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import { AppState, Repository, Release, AIConfig, WebDAVConfig, SearchFilters, GitHubUser, Category, AssetFilter } from '../types';
|
import { AppState, Repository, Release, AIConfig, WebDAVConfig, SearchFilters, GitHubUser, Category, AssetFilter, UpdateNotification } from '../types';
|
||||||
|
|
||||||
interface AppActions {
|
interface AppActions {
|
||||||
// Auth actions
|
// Auth actions
|
||||||
@@ -52,6 +52,10 @@ interface AppActions {
|
|||||||
setTheme: (theme: 'light' | 'dark') => void;
|
setTheme: (theme: 'light' | 'dark') => void;
|
||||||
setCurrentView: (view: 'repositories' | 'releases' | 'settings') => void;
|
setCurrentView: (view: 'repositories' | 'releases' | 'settings') => void;
|
||||||
setLanguage: (language: 'zh' | 'en') => void;
|
setLanguage: (language: 'zh' | 'en') => void;
|
||||||
|
|
||||||
|
// Update actions
|
||||||
|
setUpdateNotification: (notification: UpdateNotification | null) => void;
|
||||||
|
dismissUpdateNotification: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialSearchFilters: SearchFilters = {
|
const initialSearchFilters: SearchFilters = {
|
||||||
@@ -177,6 +181,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
theme: 'light',
|
theme: 'light',
|
||||||
currentView: 'repositories',
|
currentView: 'repositories',
|
||||||
language: 'zh',
|
language: 'zh',
|
||||||
|
updateNotification: null,
|
||||||
|
|
||||||
// Auth actions
|
// Auth actions
|
||||||
setUser: (user) => {
|
setUser: (user) => {
|
||||||
@@ -307,6 +312,10 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
setTheme: (theme) => set({ theme }),
|
setTheme: (theme) => set({ theme }),
|
||||||
setCurrentView: (currentView) => set({ currentView }),
|
setCurrentView: (currentView) => set({ currentView }),
|
||||||
setLanguage: (language) => set({ language }),
|
setLanguage: (language) => set({ language }),
|
||||||
|
|
||||||
|
// Update actions
|
||||||
|
setUpdateNotification: (notification) => set({ updateNotification: notification }),
|
||||||
|
dismissUpdateNotification: () => set({ updateNotification: null }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'github-stars-manager',
|
name: 'github-stars-manager',
|
||||||
|
|||||||
@@ -152,4 +152,15 @@ export interface AppState {
|
|||||||
theme: 'light' | 'dark';
|
theme: 'light' | 'dark';
|
||||||
currentView: 'repositories' | 'releases' | 'settings';
|
currentView: 'repositories' | 'releases' | 'settings';
|
||||||
language: 'zh' | 'en';
|
language: 'zh' | 'en';
|
||||||
|
|
||||||
|
// Update
|
||||||
|
updateNotification: UpdateNotification | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateNotification {
|
||||||
|
version: string;
|
||||||
|
releaseDate: string;
|
||||||
|
changelog: string[];
|
||||||
|
downloadUrl: string;
|
||||||
|
dismissed: boolean;
|
||||||
}
|
}
|
||||||
216
test-update.html
Normal file
216
test-update.html
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>更新功能测试</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.version-info {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.changelog {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.changelog li {
|
||||||
|
padding: 5px 0;
|
||||||
|
border-left: 3px solid #007AFF;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #007AFF;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #0056CC;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #FF3B30;
|
||||||
|
background: #FFE5E5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
color: #34C759;
|
||||||
|
background: #E5F7E5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>GitHub Stars Manager - 更新功能测试</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>当前版本: v0.1.3</h2>
|
||||||
|
<button id="checkUpdate" onclick="checkForUpdates()">检查更新</button>
|
||||||
|
<div id="status"></div>
|
||||||
|
<div id="updateInfo"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 模拟UpdateService的功能
|
||||||
|
class UpdateService {
|
||||||
|
static REPO_URL = './versions/version-info.xml'; // 本地测试用
|
||||||
|
static CURRENT_VERSION = '0.1.3';
|
||||||
|
|
||||||
|
static async checkForUpdates() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.REPO_URL);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const xmlText = await response.text();
|
||||||
|
const versions = this.parseVersionXML(xmlText);
|
||||||
|
|
||||||
|
if (versions.length === 0) {
|
||||||
|
return {
|
||||||
|
hasUpdate: false,
|
||||||
|
currentVersion: this.CURRENT_VERSION
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestVersion = versions[versions.length - 1];
|
||||||
|
const hasUpdate = this.compareVersions(this.CURRENT_VERSION, latestVersion.number) < 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasUpdate,
|
||||||
|
currentVersion: this.CURRENT_VERSION,
|
||||||
|
latestVersion: hasUpdate ? latestVersion : undefined
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查更新失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static parseVersionXML(xmlText) {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
||||||
|
|
||||||
|
const parseError = xmlDoc.querySelector('parsererror');
|
||||||
|
if (parseError) {
|
||||||
|
throw new Error('XML解析失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = [];
|
||||||
|
const versionNodes = xmlDoc.querySelectorAll('version');
|
||||||
|
|
||||||
|
versionNodes.forEach(versionNode => {
|
||||||
|
const number = versionNode.querySelector('number')?.textContent?.trim();
|
||||||
|
const releaseDate = versionNode.querySelector('releaseDate')?.textContent?.trim();
|
||||||
|
const downloadUrl = versionNode.querySelector('downloadUrl')?.textContent?.trim();
|
||||||
|
|
||||||
|
if (!number || !releaseDate || !downloadUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changelog = [];
|
||||||
|
const changelogItems = versionNode.querySelectorAll('changelog item');
|
||||||
|
changelogItems.forEach(item => {
|
||||||
|
const text = item.textContent?.trim();
|
||||||
|
if (text) {
|
||||||
|
changelog.push(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
versions.push({
|
||||||
|
number,
|
||||||
|
releaseDate,
|
||||||
|
changelog,
|
||||||
|
downloadUrl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
static compareVersions(version1, version2) {
|
||||||
|
const v1Parts = version1.split('.').map(Number);
|
||||||
|
const v2Parts = version2.split('.').map(Number);
|
||||||
|
|
||||||
|
const maxLength = Math.max(v1Parts.length, v2Parts.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
const v1Part = v1Parts[i] || 0;
|
||||||
|
const v2Part = v2Parts[i] || 0;
|
||||||
|
|
||||||
|
if (v1Part < v2Part) return -1;
|
||||||
|
if (v1Part > v2Part) return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates() {
|
||||||
|
const button = document.getElementById('checkUpdate');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
const updateInfo = document.getElementById('updateInfo');
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = '检查中...';
|
||||||
|
status.innerHTML = '';
|
||||||
|
updateInfo.innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await UpdateService.checkForUpdates();
|
||||||
|
|
||||||
|
if (result.hasUpdate && result.latestVersion) {
|
||||||
|
status.innerHTML = '<div class="success">发现新版本!</div>';
|
||||||
|
|
||||||
|
const version = result.latestVersion;
|
||||||
|
updateInfo.innerHTML = `
|
||||||
|
<div class="version-info">
|
||||||
|
<h3>版本 ${version.number}</h3>
|
||||||
|
<p><strong>发布日期:</strong> ${version.releaseDate}</p>
|
||||||
|
<p><strong>更新内容:</strong></p>
|
||||||
|
<ul class="changelog">
|
||||||
|
${version.changelog.map(item => `<li>${item}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
<button onclick="window.open('${version.downloadUrl}', '_blank')">
|
||||||
|
立即下载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
status.innerHTML = '<div class="success">当前已是最新版本!</div>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
status.innerHTML = `<div class="error">检查更新失败: ${error.message}</div>`;
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = '检查更新';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时自动检查一次更新
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
setTimeout(checkForUpdates, 1000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
67
versions/README.md
Normal file
67
versions/README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# 版本管理说明
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
- `version-info.xml` - 存储所有版本信息的XML文件
|
||||||
|
- `README.md` - 本说明文件
|
||||||
|
|
||||||
|
## 版本更新流程
|
||||||
|
|
||||||
|
### 1. 更新版本信息
|
||||||
|
|
||||||
|
使用脚本自动更新版本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run update-version 0.1.3 "修复搜索功能bug" "添加新的过滤选项" "优化界面响应速度"
|
||||||
|
```
|
||||||
|
|
||||||
|
这个命令会:
|
||||||
|
- 更新 `package.json` 中的版本号
|
||||||
|
- 在 `version-info.xml` 中添加新版本记录
|
||||||
|
- 更新 `src/services/updateService.ts` 中的当前版本号
|
||||||
|
|
||||||
|
### 2. 手动更新(不推荐)
|
||||||
|
|
||||||
|
如果需要手动更新 `version-info.xml`,请按照以下格式:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<versions>
|
||||||
|
<version>
|
||||||
|
<number>0.1.3</number>
|
||||||
|
<releaseDate>2025-01-03</releaseDate>
|
||||||
|
<changelog>
|
||||||
|
<item>修复搜索功能bug</item>
|
||||||
|
<item>添加新的过滤选项</item>
|
||||||
|
<item>优化界面响应速度</item>
|
||||||
|
</changelog>
|
||||||
|
<downloadUrl>https://github.com/AmintaCCCP/GithubStarsManager/releases/download/v0.1.3/github-stars-manager-0.1.3.dmg</downloadUrl>
|
||||||
|
</version>
|
||||||
|
</versions>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 发布流程
|
||||||
|
|
||||||
|
1. 使用 `npm run update-version` 更新版本信息
|
||||||
|
2. 提交更改到 Git 仓库:
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "chore: bump version to v0.1.3"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
3. 在 GitHub 上创建对应的 Release,并上传构建好的安装包
|
||||||
|
4. 确保下载链接与 XML 中的 `downloadUrl` 一致
|
||||||
|
|
||||||
|
## XML 文件格式说明
|
||||||
|
|
||||||
|
- `number`: 版本号,格式为 x.y.z
|
||||||
|
- `releaseDate`: 发布日期,格式为 YYYY-MM-DD
|
||||||
|
- `changelog`: 更新日志,每个 `<item>` 代表一条更新内容
|
||||||
|
- `downloadUrl`: 对应版本的下载链接
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 版本号必须遵循语义化版本规范(Semantic Versioning)
|
||||||
|
2. 每次发布新版本时,确保 GitHub Release 中的下载链接可用
|
||||||
|
3. XML 文件会被应用程序通过网络请求读取,确保文件格式正确
|
||||||
|
4. 建议在发布前先在本地测试更新检查功能
|
||||||
Reference in New Issue
Block a user