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",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "github-stars-manager",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"date-fns": "^3.3.1",
|
||||
"lucide-react": "^0.344.0",
|
||||
@@ -29,7 +29,8 @@
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"vite": "^5.4.2"
|
||||
"vite": "^5.4.2",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -3449,6 +3450,13 @@
|
||||
"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": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
@@ -4071,6 +4079,30 @@
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "github-stars-manager",
|
||||
"private": true,
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -11,15 +11,16 @@
|
||||
"build:desktop": "node scripts/build-desktop.js",
|
||||
"electron": "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": {
|
||||
"date-fns": "^3.3.1",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"zustand": "^4.5.0",
|
||||
"date-fns": "^3.3.1"
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
@@ -35,6 +36,7 @@
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.5.3",
|
||||
"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 { SettingsPanel } from './components/SettingsPanel';
|
||||
import { useAppStore } from './store/useAppStore';
|
||||
import { useAutoUpdateCheck } from './components/UpdateChecker';
|
||||
import { UpdateNotificationBanner } from './components/UpdateNotificationBanner';
|
||||
|
||||
function App() {
|
||||
const {
|
||||
@@ -19,6 +21,9 @@ function App() {
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
|
||||
// 自动检查更新
|
||||
useAutoUpdateCheck();
|
||||
|
||||
// Apply theme to document
|
||||
useEffect(() => {
|
||||
if (theme === 'dark') {
|
||||
@@ -64,6 +69,7 @@ function App() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<UpdateNotificationBanner />
|
||||
<Header />
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{renderCurrentView()}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { GitHubApiService } from '../services/githubApi';
|
||||
|
||||
@@ -159,17 +159,6 @@ export const Header: React.FC = () => {
|
||||
|
||||
{/* User Actions */}
|
||||
<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 */}
|
||||
<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>
|
||||
|
||||
@@ -14,12 +14,18 @@ import {
|
||||
Upload,
|
||||
RefreshCw,
|
||||
Globe,
|
||||
MessageSquare
|
||||
MessageSquare,
|
||||
Package,
|
||||
ExternalLink,
|
||||
Mail,
|
||||
Github,
|
||||
Twitter
|
||||
} from 'lucide-react';
|
||||
import { AIConfig, WebDAVConfig } from '../types';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { AIService } from '../services/aiService';
|
||||
import { WebDAVService } from '../services/webdavService';
|
||||
import { UpdateChecker } from './UpdateChecker';
|
||||
|
||||
export const SettingsPanel: React.FC = () => {
|
||||
const {
|
||||
@@ -351,6 +357,28 @@ Focus on practicality and accurate categorization to help users quickly understa
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<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">
|
||||
@@ -390,6 +418,42 @@ Focus on practicality and accurate categorization to help users quickly understa
|
||||
</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 */}
|
||||
<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">
|
||||
|
||||
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 { 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 {
|
||||
// Auth actions
|
||||
@@ -52,6 +52,10 @@ interface AppActions {
|
||||
setTheme: (theme: 'light' | 'dark') => void;
|
||||
setCurrentView: (view: 'repositories' | 'releases' | 'settings') => void;
|
||||
setLanguage: (language: 'zh' | 'en') => void;
|
||||
|
||||
// Update actions
|
||||
setUpdateNotification: (notification: UpdateNotification | null) => void;
|
||||
dismissUpdateNotification: () => void;
|
||||
}
|
||||
|
||||
const initialSearchFilters: SearchFilters = {
|
||||
@@ -177,6 +181,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
theme: 'light',
|
||||
currentView: 'repositories',
|
||||
language: 'zh',
|
||||
updateNotification: null,
|
||||
|
||||
// Auth actions
|
||||
setUser: (user) => {
|
||||
@@ -307,6 +312,10 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
setTheme: (theme) => set({ theme }),
|
||||
setCurrentView: (currentView) => set({ currentView }),
|
||||
setLanguage: (language) => set({ language }),
|
||||
|
||||
// Update actions
|
||||
setUpdateNotification: (notification) => set({ updateNotification: notification }),
|
||||
dismissUpdateNotification: () => set({ updateNotification: null }),
|
||||
}),
|
||||
{
|
||||
name: 'github-stars-manager',
|
||||
|
||||
@@ -152,4 +152,15 @@ export interface AppState {
|
||||
theme: 'light' | 'dark';
|
||||
currentView: 'repositories' | 'releases' | 'settings';
|
||||
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