This commit is contained in:
AmintaCCCP
2025-08-03 16:49:39 +08:00
parent edb68290c9
commit 682695f1d1
14 changed files with 1184 additions and 23 deletions

148
UPDATE_FEATURE_GUIDE.md Normal file
View 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
View File

@@ -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",

View File

@@ -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
View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
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();

View File

@@ -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()}

View File

@@ -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>

View File

@@ -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">

View 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]);
};

View 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>
);
};

View 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');
}
}

View File

@@ -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',

View File

@@ -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
View 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
View 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. 建议在发布前先在本地测试更新检查功能