mirror of
https://github.com/AmintaCCCP/GithubStarsManager.git
synced 2025-11-25 18:47:32 +08:00
Compare commits
39 Commits
v0.1.2-fix
...
v0.1.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6136d6ee29 | ||
|
|
627667750a | ||
|
|
b0982f8358 | ||
|
|
9ed8583daa | ||
|
|
eaefc7f351 | ||
|
|
8c5f71ea77 | ||
|
|
d78bcd75d6 | ||
|
|
e0af19dd2e | ||
|
|
0678fe9b04 | ||
|
|
69f4a0788c | ||
|
|
b2c49460ab | ||
|
|
b7ad4558ef | ||
|
|
e095d955e1 | ||
|
|
babe33e616 | ||
|
|
1b914584e3 | ||
|
|
deb015ca8c | ||
|
|
36636c5d31 | ||
|
|
07684356b4 | ||
|
|
d4475a644d | ||
|
|
f5d7819fc7 | ||
|
|
724bce3ff4 | ||
|
|
e49d20dcdb | ||
|
|
da13c7b759 | ||
|
|
7cddb5e480 | ||
|
|
83bf2d9334 | ||
|
|
3272ff2d66 | ||
|
|
ca65dc53ec | ||
|
|
3783e120ad | ||
|
|
3372552391 | ||
|
|
4ef03f9dec | ||
|
|
0b5d01fbb2 | ||
|
|
83bbc588db | ||
|
|
73c9f9ec9e | ||
|
|
28cd6defae | ||
|
|
682695f1d1 | ||
|
|
edb68290c9 | ||
|
|
a9e8d8ce15 | ||
|
|
a6c39b133c | ||
|
|
f811326705 |
13
.github/workflows/build-desktop.yml
vendored
13
.github/workflows/build-desktop.yml
vendored
@@ -33,8 +33,8 @@ jobs:
|
||||
|
||||
- name: Build web app
|
||||
run: npm run build
|
||||
env:
|
||||
VITE_BASE_PATH: './'
|
||||
# env:
|
||||
# VITE_BASE_PATH: './'
|
||||
|
||||
- name: Verify and fix web build
|
||||
shell: bash
|
||||
@@ -409,7 +409,8 @@ jobs:
|
||||
icon: 'build/icon.png',
|
||||
category: 'public.app-category.productivity',
|
||||
hardenedRuntime: true,
|
||||
gatekeeperAssess: false
|
||||
gatekeeperAssess: false,
|
||||
identity: null
|
||||
};
|
||||
packageJson.build.dmg = {
|
||||
title: 'GitHub Stars Manager',
|
||||
@@ -467,6 +468,12 @@ jobs:
|
||||
DEBUG: electron-builder
|
||||
# Linux 特定环境变量
|
||||
ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
|
||||
# macOS signing and notarization
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
- name: List build output
|
||||
shell: bash
|
||||
|
||||
46
.github/workflows/build-web.yml
vendored
46
.github/workflows/build-web.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Build and Deploy Web App
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build web app
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: web-build
|
||||
path: dist/
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./dist
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,6 +9,7 @@ lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
@@ -23,3 +24,5 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
release
|
||||
electron
|
||||
66
DOCKER.md
Normal file
66
DOCKER.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Docker Deployment
|
||||
|
||||
This application can be deployed using Docker with minimal configuration. The Docker setup serves the static frontend files via Nginx and handles CORS properly.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed on your system
|
||||
- Docker Compose (optional, but recommended)
|
||||
|
||||
## Building and Running with Docker
|
||||
|
||||
### Using Docker Compose (Recommended)
|
||||
|
||||
```bash
|
||||
# Build and start the container
|
||||
docker-compose up -d
|
||||
|
||||
# The application will be available at http://localhost:8080
|
||||
```
|
||||
|
||||
### Using Docker directly
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t github-stars-manager .
|
||||
|
||||
# Run the container
|
||||
docker run -d -p 8080:80 --name github-stars-manager github-stars-manager
|
||||
|
||||
# The application will be available at http://localhost:8080
|
||||
```
|
||||
|
||||
## CORS Handling
|
||||
|
||||
This Docker setup handles CORS in two ways:
|
||||
|
||||
1. **Nginx CORS Headers**: The Nginx configuration adds appropriate CORS headers to allow API calls to external services.
|
||||
|
||||
2. **Client-Side Handling**: The application is designed to work with any AI or WebDAV service URL configured by the user, without requiring proxying.
|
||||
|
||||
## Configuration
|
||||
|
||||
No special configuration is needed for the Docker container itself. All application settings (API URLs, credentials, etc.) are configured through the application UI.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
While not required, you can pass environment variables to the container if needed:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:80 -e NODE_ENV=production --name github-stars-manager github-stars-manager
|
||||
```
|
||||
|
||||
## Stopping the Container
|
||||
|
||||
```bash
|
||||
# With Docker Compose
|
||||
docker-compose down
|
||||
|
||||
# With Docker directly
|
||||
docker stop github-stars-manager
|
||||
docker rm github-stars-manager
|
||||
```
|
||||
|
||||
## Note on Desktop Packaging
|
||||
|
||||
This Docker setup does not affect the existing desktop packaging workflows. The GitHub Actions workflow for building desktop applications remains unchanged and continues to work as before.
|
||||
103
DOCKER_IMPLEMENTATION_SUMMARY.md
Normal file
103
DOCKER_IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Docker Implementation Summary
|
||||
|
||||
This document summarizes the Docker implementation for the GitHub Stars Manager application.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Files Created
|
||||
|
||||
1. **Dockerfile** - Multi-stage build process:
|
||||
- Build stage: Uses Node.js 18 Alpine to build the React application
|
||||
- Production stage: Uses Nginx Alpine to serve the static files
|
||||
|
||||
2. **nginx.conf** - Custom Nginx configuration:
|
||||
- Handles CORS headers properly for API calls
|
||||
- Serves static files with proper caching headers
|
||||
- Implements SPA routing with try_files directive
|
||||
- Adds security headers
|
||||
|
||||
3. **docker-compose.yml** - Docker Compose configuration:
|
||||
- Simplifies deployment with a single command
|
||||
- Maps port 8080 to container port 80
|
||||
|
||||
4. **DOCKER.md** - Detailed documentation:
|
||||
- Instructions for building and running with Docker
|
||||
- Explanation of CORS handling approach
|
||||
- Configuration guidance
|
||||
|
||||
5. **test-docker.html** - Simple test page:
|
||||
- Verifies that the application is accessible
|
||||
|
||||
### Key Features
|
||||
|
||||
1. **Minimal Changes**: The Docker setup doesn't affect the existing desktop packaging workflows or GitHub Actions.
|
||||
|
||||
2. **CORS Handling**:
|
||||
- Nginx adds appropriate CORS headers to allow API calls to external services
|
||||
- No proxying is used, allowing users to configure any AI or WebDAV service URLs
|
||||
|
||||
3. **Static File Serving**:
|
||||
- Optimized Nginx configuration for serving static React applications
|
||||
- Proper caching headers for better performance
|
||||
- SPA routing support
|
||||
|
||||
4. **Flexibility**:
|
||||
- Works with any AI service that supports OpenAI-compatible APIs
|
||||
- Works with any WebDAV service
|
||||
- No hardcoded API URLs or endpoints
|
||||
|
||||
### Testing Performed
|
||||
|
||||
1. ✅ Docker image builds successfully
|
||||
2. ✅ Container runs and serves files on port 8080
|
||||
3. ✅ Docker Compose setup works correctly
|
||||
4. ✅ CORS headers are properly configured
|
||||
5. ✅ Static files are served correctly
|
||||
6. ✅ SPA routing works correctly
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Build Process**: The Dockerfile uses a multi-stage build:
|
||||
- First stage installs Node.js dependencies and builds the React app
|
||||
- Second stage copies the built files to an Nginx container
|
||||
|
||||
2. **Runtime**: The Nginx server:
|
||||
- Serves static files from `/usr/share/nginx/html`
|
||||
- Handles CORS with appropriate headers
|
||||
- Routes all requests to index.html for SPA functionality
|
||||
|
||||
3. **API Calls**:
|
||||
- The application makes direct calls to AI and WebDAV services
|
||||
- Nginx adds CORS headers to allow these cross-origin requests
|
||||
- Users can configure any service URLs in the application UI
|
||||
|
||||
### Advantages
|
||||
|
||||
1. **No Proxy Required**: Unlike development setups, this production setup doesn't need proxying since the browser considers all requests as coming from the same origin (the Docker container).
|
||||
|
||||
2. **Dynamic URL Support**: Users can configure any AI or WebDAV service URLs without rebuilding the container.
|
||||
|
||||
3. **Performance**: Nginx is highly efficient for serving static files.
|
||||
|
||||
4. **Compatibility**: Doesn't interfere with existing desktop packaging workflows.
|
||||
|
||||
### Usage Instructions
|
||||
|
||||
1. **With Docker Compose** (recommended):
|
||||
```bash
|
||||
docker-compose up -d
|
||||
# Application available at http://localhost:8080
|
||||
```
|
||||
|
||||
2. **With Docker directly**:
|
||||
```bash
|
||||
docker build -t github-stars-manager .
|
||||
docker run -d -p 8080:80 github-stars-manager
|
||||
# Application available at http://localhost:8080
|
||||
```
|
||||
|
||||
The implementation satisfies all the requirements:
|
||||
- ✅ Minimal changes to existing codebase
|
||||
- ✅ Doesn't affect desktop packaging workflows
|
||||
- ✅ Handles CORS properly for API calls
|
||||
- ✅ Supports dynamic API URLs configured by users
|
||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# Build stage
|
||||
FROM node:18-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy built files from build stage
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy custom nginx configuration
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -146,4 +146,19 @@ if (selectedFilters.length > 0) {
|
||||
- 点击文件名直接下载
|
||||
- 查看文件详细信息
|
||||
|
||||
## Docker 部署支持
|
||||
|
||||
### 新增文件
|
||||
1. **Dockerfile** - 多阶段构建配置
|
||||
2. **nginx.conf** - Nginx 服务器配置,包含 CORS 头设置
|
||||
3. **docker-compose.yml** - Docker Compose 配置文件
|
||||
4. **DOCKER.md** - 详细部署文档
|
||||
5. **DOCKER_IMPLEMENTATION_SUMMARY.md** - 实现总结
|
||||
|
||||
### 功能特点
|
||||
- 通过 Nginx 正确处理 CORS,支持任意 AI/WebDAV 服务 URL
|
||||
- 不影响现有的桌面应用打包流程
|
||||
- 支持 Docker 和 Docker Compose 两种部署方式
|
||||
- 静态文件优化服务
|
||||
|
||||
这个实现完全满足了用户的需求,提供了更灵活、更直观的 Release 文件管理和下载体验。
|
||||
47
README.md
47
README.md
@@ -1,13 +1,35 @@
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
# GithubStarsManager
|
||||
|
||||
  
|
||||
|
||||
|
||||
An app for managing github starred repositories.
|
||||
|
||||
<a href="https://www.producthunt.com/products/githubstarsmanager?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-githubstarsmanager" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1001489&theme=light&t=1754373322417" alt="GithubStarsManager - AI organizes GitHub stars for easy find | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
</div>
|
||||
|
||||
## ✨ Features
|
||||
|
||||
Tired of starring everything and finding nothing? GitHub Stars Manager automatically syncs your starred repos, uses AI to summarize and categorize them, and lets you find anything with semantic search. Track releases, filter assets, and one‑click download—smarter than manual tags, simpler than GitHub.
|
||||
|
||||
- Auto-sync stars: connect your GitHub token to pull all starred repos
|
||||
- AI summaries & categories: generate tags, topics, and short README overviews
|
||||
- Semantic search: find repos by intent, not exact names
|
||||
- Release tracking: subscribe to repos and see new versions in one place
|
||||
- One‑click downloads: expand release assets and download instantly
|
||||
- Smart filters: match assets by keywords (e.g., dmg/mac/arm64/aarch64)
|
||||
- Bilingual wiki jump: deepwiki (EN) or zread (ZH) based on language
|
||||
- Packaged client: no environment setup required
|
||||
|
||||
### Starred Repo Manager
|
||||
|
||||
1. Automatically pull the starred repositories under your github account. You can use AI to automatically analyze the repository and automatically generate repository descriptions, labels, and classifications.
|
||||
2. through the filter, **natural language search**, you can quickly find the repository.
|
||||
2. through the filter, keyword search, you can quickly find the repository.
|
||||
|
||||

|
||||
|
||||
@@ -25,16 +47,35 @@ Use your own AI model API that supports OpenAI-compatible interfaces.
|
||||
|
||||
## 👋🏻 How to Use
|
||||
|
||||
### 💻 Desktop Client (Recommended)
|
||||
|
||||
You can download desktop client here:
|
||||
https://github.com/AmintaCCCP/GithubStarsManager/releases
|
||||
|
||||
### 🤖 Run With code
|
||||
|
||||
1. Download the source code, or clone the repository
|
||||
2. Navigate to the directory, and open a Terminal window at the downloaded folder.
|
||||
3. Run `npm install` to install dependencies and `npm run dev` to build
|
||||
|
||||
> 💡 When running the project locally using `npm run dev`, calls to AI services and WebDAV may fail due to CORS restrictions. To avoid this issue, use the prebuilt client application or build the client yourself.
|
||||
> You can also download desktop client for MacOS:
|
||||
> https://github.com/AmintaCCCP/GithubStarsManager/releases
|
||||
|
||||
### 🐳 Run With Docker
|
||||
|
||||
You can also run this application using Docker. See [DOCKER.md](DOCKER.md) for detailed instructions on how to build and deploy using Docker. The Docker setup handles CORS properly and allows you to configure any AI or WebDAV service URLs directly in the application.
|
||||
|
||||
|
||||
## Who it’s for
|
||||
|
||||
Developers with hundreds/thousands of stars
|
||||
People who systematically track releases
|
||||
“Lazy-efficient” users who don’t want manual tagging
|
||||
|
||||
## Additional Notes
|
||||
|
||||
1. There is no backend for this app, so save your important data on your own.
|
||||
2. I can't write code, this app is entirely written by the AI, mainly for my personal requirment. If you have a new feature or meet a bug, I can only try to do it, but I can't guarantee it, because it depends on the AI to do it successfully.😹
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#AmintaCCCP/GithubStarsManager&Date)
|
||||
@@ -143,6 +143,9 @@ npm run build
|
||||
- Cloudflare Pages
|
||||
- 自建服务器
|
||||
|
||||
### Docker 部署
|
||||
您也可以使用 Docker 来运行此应用程序。请参阅 [DOCKER.md](DOCKER.md) 获取详细的构建和部署说明。Docker 设置正确处理了 CORS,并允许您直接在应用程序中配置任何 AI 或 WebDAV 服务 URL。
|
||||
|
||||
## 贡献 / Contributing
|
||||
|
||||
欢迎提交Issue和Pull Request!
|
||||
|
||||
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
|
||||
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -10,8 +10,8 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<!-- Material Icons CDN -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<script type="module" crossorigin src="/assets/index-CQ4S0AeE.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BQKjD-gi.css">
|
||||
<script type="module" crossorigin src="/assets/index-DZXyNObJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BWYOfSoc.css">
|
||||
</head>
|
||||
<body class="bg-gray-50 dark:bg-gray-900">
|
||||
<div id="root"></div>
|
||||
|
||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
github-stars-manager:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:80"
|
||||
restart: unless-stopped
|
||||
# Environment variables can be set here if needed
|
||||
# environment:
|
||||
# - NODE_ENV=production
|
||||
76
nginx.conf
Normal file
76
nginx.conf
Normal file
@@ -0,0 +1,76 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Hide nginx version
|
||||
server_tokens off;
|
||||
|
||||
# Log format
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Include additional configuration files
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
# Server block
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Handle preflight requests for CORS
|
||||
location / {
|
||||
# Handle preflight requests
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
||||
add_header 'Access-Control-Max-Age' 1728000 always;
|
||||
add_header 'Content-Type' 'text/plain; charset=utf-8' always;
|
||||
add_header 'Content-Length' 0 always;
|
||||
return 204;
|
||||
}
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
# Try to serve file, if not found, serve index.html (for SPA routing)
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Add CORS headers for all responses
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
|
||||
|
||||
# Add security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
}
|
||||
|
||||
# Error pages
|
||||
error_page 404 /404.html;
|
||||
location = /404.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
internal;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
package-lock.json
generated
40
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",
|
||||
@@ -4130,4 +4162,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "github-stars-manager",
|
||||
"private": true,
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.6",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ function createWindow() {
|
||||
enableRemoteModule: false,
|
||||
webSecurity: true
|
||||
},
|
||||
icon: path.join(__dirname, '../dist/vite.svg'),
|
||||
icon: path.join(__dirname, '../dist/icon.svg'),
|
||||
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
|
||||
show: false
|
||||
});
|
||||
|
||||
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()}
|
||||
|
||||
@@ -102,7 +102,7 @@ export const CategorySidebar: React.FC<CategorySidebarProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-64 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 h-fit sticky top-24">
|
||||
<div className="w-64 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 max-h-[calc(100vh-8rem)] sticky top-24 overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t('应用分类', 'Categories')}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -98,7 +98,7 @@ export const Header: React.FC = () => {
|
||||
const t = (zh: string, en: string) => language === 'zh' ? zh : en;
|
||||
|
||||
return (
|
||||
<header className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-50">
|
||||
<header className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-50 hd-drag">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo and Title */}
|
||||
@@ -121,7 +121,7 @@ export const Header: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="hidden md:flex items-center space-x-1">
|
||||
<nav className="hidden md:flex items-center space-x-1 hd-btns">
|
||||
<button
|
||||
onClick={() => setCurrentView('repositories')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
@@ -158,18 +158,7 @@ export const Header: React.FC = () => {
|
||||
</nav>
|
||||
|
||||
{/* 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>
|
||||
|
||||
<div className="flex items-center space-x-3 hd-btns">
|
||||
{/* 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>
|
||||
|
||||
@@ -197,7 +197,8 @@ export const RepositoryCard: React.FC<RepositoryCardProps> = ({
|
||||
ai_summary: analysis.summary,
|
||||
ai_tags: analysis.tags,
|
||||
ai_platforms: analysis.platforms,
|
||||
analyzed_at: new Date().toISOString()
|
||||
analyzed_at: new Date().toISOString(),
|
||||
analysis_failed: false // 分析成功,清除失败标记
|
||||
};
|
||||
|
||||
updateRepository(updatedRepo);
|
||||
@@ -209,6 +210,16 @@ export const RepositoryCard: React.FC<RepositoryCardProps> = ({
|
||||
alert(successMessage);
|
||||
} catch (error) {
|
||||
console.error('AI analysis failed:', error);
|
||||
|
||||
// 标记为分析失败
|
||||
const failedRepo = {
|
||||
...repository,
|
||||
analyzed_at: new Date().toISOString(),
|
||||
analysis_failed: true
|
||||
};
|
||||
|
||||
updateRepository(failedRepo);
|
||||
|
||||
alert(language === 'zh' ? 'AI分析失败,请检查AI配置和网络连接。' : 'AI analysis failed. Please check AI configuration and network connection.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -232,6 +243,12 @@ export const RepositoryCard: React.FC<RepositoryCardProps> = ({
|
||||
content: repository.custom_description,
|
||||
isCustom: true
|
||||
};
|
||||
} else if (showAISummary && repository.analysis_failed) {
|
||||
return {
|
||||
content: repository.description || (language === 'zh' ? '暂无描述' : 'No description available'),
|
||||
isAI: false,
|
||||
isFailed: true
|
||||
};
|
||||
} else if (showAISummary && repository.ai_summary) {
|
||||
return {
|
||||
content: repository.ai_summary,
|
||||
@@ -277,7 +294,12 @@ export const RepositoryCard: React.FC<RepositoryCardProps> = ({
|
||||
|
||||
// 获取AI分析按钮的提示文本
|
||||
const getAIButtonTitle = () => {
|
||||
if (repository.analyzed_at) {
|
||||
if (repository.analysis_failed) {
|
||||
const analyzeTime = new Date(repository.analyzed_at!).toLocaleString();
|
||||
return language === 'zh'
|
||||
? `分析失败于 ${analyzeTime},点击重新分析`
|
||||
: `Analysis failed on ${analyzeTime}, click to retry`;
|
||||
} else if (repository.analyzed_at) {
|
||||
const analyzeTime = new Date(repository.analyzed_at).toLocaleString();
|
||||
return language === 'zh'
|
||||
? `已于 ${analyzeTime} 分析过,点击重新分析`
|
||||
@@ -315,9 +337,12 @@ export const RepositoryCard: React.FC<RepositoryCardProps> = ({
|
||||
<button
|
||||
onClick={handleAIAnalyze}
|
||||
disabled={isLoading}
|
||||
className={`p-2 rounded-lg transition-colors ${repository.analyzed_at
|
||||
? 'bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-400 hover:bg-green-200 dark:hover:bg-green-800'
|
||||
: 'bg-purple-100 text-purple-600 dark:bg-purple-900 dark:text-purple-400 hover:bg-purple-200 dark:hover:bg-purple-800'
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
repository.analysis_failed
|
||||
? 'bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-800'
|
||||
: repository.analyzed_at
|
||||
? 'bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-400 hover:bg-green-200 dark:hover:bg-green-800'
|
||||
: 'bg-purple-100 text-purple-600 dark:bg-purple-900 dark:text-purple-400 hover:bg-purple-200 dark:hover:bg-purple-800'
|
||||
} disabled:opacity-50`}
|
||||
title={getAIButtonTitle()}
|
||||
>
|
||||
@@ -398,7 +423,13 @@ export const RepositoryCard: React.FC<RepositoryCardProps> = ({
|
||||
<span>{language === 'zh' ? '自定义' : 'Custom'}</span>
|
||||
</div>
|
||||
)}
|
||||
{displayContent.isAI && (
|
||||
{displayContent.isFailed && (
|
||||
<div className="flex items-center space-x-1 text-xs text-red-600 dark:text-red-400">
|
||||
<Bot className="w-3 h-3" />
|
||||
<span>{language === 'zh' ? '分析失败' : 'Analysis Failed'}</span>
|
||||
</div>
|
||||
)}
|
||||
{displayContent.isAI && !displayContent.isFailed && (
|
||||
<div className="flex items-center space-x-1 text-xs text-green-600 dark:text-green-400">
|
||||
<Bot className="w-3 h-3" />
|
||||
<span>{language === 'zh' ? 'AI总结' : 'AI Summary'}</span>
|
||||
@@ -410,8 +441,7 @@ export const RepositoryCard: React.FC<RepositoryCardProps> = ({
|
||||
{/* Category Display */}
|
||||
{displayCategory && (
|
||||
<div className="mb-3">
|
||||
<span className="inline-flex items-center px-2 py-1 bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300 rounded-md text-xs font-medium">
|
||||
<Tag className="w-3 h-3 mr-1" />
|
||||
<span className="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-md text-xs font-medium">
|
||||
{displayCategory}
|
||||
</span>
|
||||
</div>
|
||||
@@ -423,13 +453,8 @@ export const RepositoryCard: React.FC<RepositoryCardProps> = ({
|
||||
{displayTags.tags.slice(0, 3).map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={`px-2 py-1 rounded-md text-xs font-medium ${displayTags.isCustom
|
||||
? 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300'
|
||||
: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300'
|
||||
}`}
|
||||
className="px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"
|
||||
>
|
||||
{displayTags.isCustom && <Edit3 className="w-3 h-3 inline mr-1" />}
|
||||
{!displayTags.isCustom && <Tag className="w-3 h-3 inline mr-1" />}
|
||||
{highlightSearchTerm(tag, searchQuery)}
|
||||
</span>
|
||||
))}
|
||||
@@ -438,7 +463,7 @@ export const RepositoryCard: React.FC<RepositoryCardProps> = ({
|
||||
{repository.topics.slice(0, 2).map((topic, index) => (
|
||||
<span
|
||||
key={`topic-${index}`}
|
||||
className="px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-md text-xs"
|
||||
className="px-2 py-1 bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-md text-xs font-medium"
|
||||
>
|
||||
{topic}
|
||||
</span>
|
||||
@@ -500,7 +525,12 @@ export const RepositoryCard: React.FC<RepositoryCardProps> = ({
|
||||
<span>{language === 'zh' ? '已编辑' : 'Edited'}</span>
|
||||
</div>
|
||||
)}
|
||||
{repository.analyzed_at && (
|
||||
{repository.analysis_failed ? (
|
||||
<div className="flex items-center space-x-1 text-xs">
|
||||
<div className="w-2 h-2 bg-red-500 rounded-full" />
|
||||
<span>{language === 'zh' ? '分析失败' : 'Analysis failed'}</span>
|
||||
</div>
|
||||
) : repository.analyzed_at && (
|
||||
<div className="flex items-center space-x-1 text-xs">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full" />
|
||||
<span>{language === 'zh' ? 'AI已分析' : 'AI analyzed'}</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Bot, ChevronDown, Pause, Play } from 'lucide-react';
|
||||
import { RepositoryCard } from './RepositoryCard';
|
||||
|
||||
@@ -25,13 +25,13 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
updateRepository,
|
||||
language,
|
||||
customCategories,
|
||||
analysisProgress,
|
||||
setAnalysisProgress
|
||||
} = useAppStore();
|
||||
|
||||
const [showAISummary, setShowAISummary] = useState(true);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [analysisProgress, setAnalysisProgress] = useState({ current: 0, total: 0 });
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [searchTime, setSearchTime] = useState<number | undefined>(undefined);
|
||||
|
||||
// 使用 useRef 来管理停止状态,确保在异步操作中能正确访问最新值
|
||||
const shouldStopRef = useRef(false);
|
||||
@@ -75,7 +75,44 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
);
|
||||
});
|
||||
|
||||
const handleAIAnalyze = async (analyzeUnanalyzedOnly: boolean = false) => {
|
||||
// Infinite scroll (瀑布流按需加载)
|
||||
const LOAD_BATCH = 50;
|
||||
const [visibleCount, setVisibleCount] = useState(LOAD_BATCH);
|
||||
const sentinelRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const startIndex = filteredRepositories.length === 0 ? 0 : 1;
|
||||
const endIndex = Math.min(visibleCount, filteredRepositories.length);
|
||||
const visibleRepositories = filteredRepositories.slice(0, visibleCount);
|
||||
|
||||
// Reset visible count when filters or data change
|
||||
useEffect(() => {
|
||||
setVisibleCount(LOAD_BATCH);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedCategory, repositories, filteredRepositories.length]);
|
||||
|
||||
// IntersectionObserver to load more on demand
|
||||
useEffect(() => {
|
||||
const node = sentinelRef.current;
|
||||
if (!node) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry.isIntersecting) {
|
||||
setVisibleCount((count) => {
|
||||
if (count >= filteredRepositories.length) return count;
|
||||
return Math.min(count + LOAD_BATCH, filteredRepositories.length);
|
||||
});
|
||||
}
|
||||
},
|
||||
{ root: null, rootMargin: '200px', threshold: 0 }
|
||||
);
|
||||
|
||||
observer.observe(node);
|
||||
return () => observer.disconnect();
|
||||
}, [filteredRepositories.length]);
|
||||
|
||||
const handleAIAnalyze = async (analyzeUnanalyzedOnly: boolean = false, analyzeFailedOnly: boolean = false) => {
|
||||
if (!githubToken) {
|
||||
alert(language === 'zh' ? 'GitHub token 未找到,请重新登录。' : 'GitHub token not found. Please login again.');
|
||||
return;
|
||||
@@ -87,21 +124,27 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRepos = analyzeUnanalyzedOnly
|
||||
? filteredRepositories.filter(repo => !repo.analyzed_at)
|
||||
: filteredRepositories;
|
||||
const targetRepos = analyzeFailedOnly
|
||||
? filteredRepositories.filter(repo => repo.analysis_failed)
|
||||
: analyzeUnanalyzedOnly
|
||||
? filteredRepositories.filter(repo => !repo.analyzed_at)
|
||||
: filteredRepositories;
|
||||
|
||||
if (targetRepos.length === 0) {
|
||||
alert(language === 'zh'
|
||||
? (analyzeUnanalyzedOnly ? '所有仓库都已经分析过了!' : '没有可分析的仓库!')
|
||||
: (analyzeUnanalyzedOnly ? 'All repositories have been analyzed!' : 'No repositories to analyze!')
|
||||
);
|
||||
const message = analyzeFailedOnly
|
||||
? (language === 'zh' ? '没有分析失败的仓库!' : 'No failed repositories to re-analyze!')
|
||||
: analyzeUnanalyzedOnly
|
||||
? (language === 'zh' ? '所有仓库都已经分析过了!' : 'All repositories have been analyzed!')
|
||||
: (language === 'zh' ? '没有可分析的仓库!' : 'No repositories to analyze!');
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const actionText = language === 'zh'
|
||||
? (analyzeUnanalyzedOnly ? '未分析' : '全部')
|
||||
: (analyzeUnanalyzedOnly ? 'unanalyzed' : 'all');
|
||||
const actionText = analyzeFailedOnly
|
||||
? (language === 'zh' ? '失败' : 'failed')
|
||||
: analyzeUnanalyzedOnly
|
||||
? (language === 'zh' ? '未分析' : 'unanalyzed')
|
||||
: (language === 'zh' ? '全部' : 'all');
|
||||
|
||||
const confirmMessage = language === 'zh'
|
||||
? `将对 ${targetRepos.length} 个${actionText}仓库进行AI分析,这可能需要几分钟时间。是否继续?`
|
||||
@@ -126,12 +169,13 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
const customCategoryNames = customCategories.map(cat => cat.name);
|
||||
|
||||
let analyzed = 0;
|
||||
const concurrency = activeConfig.concurrency || 1;
|
||||
|
||||
for (let i = 0; i < targetRepos.length; i++) {
|
||||
// 并发分析函数
|
||||
const analyzeRepository = async (repo: Repository) => {
|
||||
// 检查是否需要停止
|
||||
if (shouldStopRef.current) {
|
||||
console.log('Analysis stopped by user');
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 处理暂停
|
||||
@@ -141,13 +185,9 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
|
||||
// 再次检查停止状态(暂停期间可能被停止)
|
||||
if (shouldStopRef.current) {
|
||||
console.log('Analysis stopped during pause');
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
|
||||
const repo = targetRepos[i];
|
||||
setAnalysisProgress({ current: i + 1, total: targetRepos.length });
|
||||
|
||||
try {
|
||||
// 获取README内容
|
||||
const [owner, name] = repo.full_name.split('/');
|
||||
@@ -162,16 +202,48 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
ai_summary: analysis.summary,
|
||||
ai_tags: analysis.tags,
|
||||
ai_platforms: analysis.platforms,
|
||||
analyzed_at: new Date().toISOString()
|
||||
analyzed_at: new Date().toISOString(),
|
||||
analysis_failed: false // 分析成功,清除失败标记
|
||||
};
|
||||
|
||||
updateRepository(updatedRepo);
|
||||
analyzed++;
|
||||
setAnalysisProgress({ current: analyzed, total: targetRepos.length });
|
||||
|
||||
// 避免API限制
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to analyze ${repo.full_name}:`, error);
|
||||
|
||||
// 标记为分析失败
|
||||
const failedRepo = {
|
||||
...repo,
|
||||
analyzed_at: new Date().toISOString(),
|
||||
analysis_failed: true
|
||||
};
|
||||
|
||||
updateRepository(failedRepo);
|
||||
analyzed++;
|
||||
setAnalysisProgress({ current: analyzed, total: targetRepos.length });
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 分批处理,支持并发
|
||||
for (let i = 0; i < targetRepos.length; i += concurrency) {
|
||||
if (shouldStopRef.current) {
|
||||
console.log('Analysis stopped by user');
|
||||
break;
|
||||
}
|
||||
|
||||
const batch = targetRepos.slice(i, i + concurrency);
|
||||
const promises = batch.map((repo) => analyzeRepository(repo));
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// 避免API限制,批次间稍作延迟
|
||||
if (i + concurrency < targetRepos.length && !shouldStopRef.current) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +329,8 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
}
|
||||
|
||||
const unanalyzedCount = filteredRepositories.filter(r => !r.analyzed_at).length;
|
||||
const analyzedCount = filteredRepositories.filter(r => r.analyzed_at).length;
|
||||
const analyzedCount = filteredRepositories.filter(r => r.analyzed_at && !r.analysis_failed).length;
|
||||
const failedCount = filteredRepositories.filter(r => r.analysis_failed).length;
|
||||
|
||||
const t = (zh: string, en: string) => language === 'zh' ? zh : en;
|
||||
|
||||
@@ -302,7 +375,7 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
<button
|
||||
onClick={() => handleAIAnalyze(true)}
|
||||
disabled={unanalyzedCount === 0}
|
||||
className="w-full px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed border-b border-gray-100 dark:border-gray-600"
|
||||
>
|
||||
<div className="font-medium text-gray-900 dark:text-white">
|
||||
{t('分析未分析的', 'Analyze Unanalyzed')}
|
||||
@@ -311,6 +384,18 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
{t(`分析 ${unanalyzedCount} 个未分析仓库`, `Analyze ${unanalyzedCount} unanalyzed repositories`)}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAIAnalyze(false, true)}
|
||||
disabled={failedCount === 0}
|
||||
className="w-full px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<div className="font-medium text-gray-900 dark:text-white">
|
||||
{t('重新分析失败的', 'Re-analyze Failed')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t(`重新分析 ${failedCount} 个失败仓库`, `Re-analyze ${failedCount} failed repositories`)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -383,7 +468,10 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
{t(`显示 ${filteredRepositories.length} 个仓库`, `Showing ${filteredRepositories.length} repositories`)}
|
||||
{t(
|
||||
`第 ${startIndex}-${endIndex} / 共 ${filteredRepositories.length} 个仓库`,
|
||||
`Showing ${startIndex}-${endIndex} of ${filteredRepositories.length} repositories`
|
||||
)}
|
||||
{repositories.length !== filteredRepositories.length && (
|
||||
<span className="ml-2 text-blue-600 dark:text-blue-400">
|
||||
{t(`(从 ${repositories.length} 个中筛选)`, `(filtered from ${repositories.length})`)}
|
||||
@@ -396,6 +484,11 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
• {analyzedCount} {t('个已AI分析', 'AI analyzed')}
|
||||
</span>
|
||||
)}
|
||||
{failedCount > 0 && (
|
||||
<span className="mr-3">
|
||||
• {failedCount} {t('个分析失败', 'analysis failed')}
|
||||
</span>
|
||||
)}
|
||||
{unanalyzedCount > 0 && (
|
||||
<span>
|
||||
• {unanalyzedCount} {t('个未分析', 'unanalyzed')}
|
||||
@@ -406,17 +499,22 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Repository Grid */}
|
||||
{/* Repository Grid with consistent card widths */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredRepositories.map(repo => (
|
||||
{visibleRepositories.map(repo => (
|
||||
<RepositoryCard
|
||||
key={repo.id}
|
||||
key={repo.id}
|
||||
repository={repo}
|
||||
showAISummary={showAISummary}
|
||||
searchQuery={useAppStore.getState().searchFilters.query}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sentinel for on-demand loading */}
|
||||
{visibleCount < filteredRepositories.length && (
|
||||
<div ref={sentinelRef} className="h-8" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 {
|
||||
@@ -62,6 +68,7 @@ export const SettingsPanel: React.FC = () => {
|
||||
model: '',
|
||||
customPrompt: '',
|
||||
useCustomPrompt: false,
|
||||
concurrency: 1,
|
||||
});
|
||||
|
||||
const [webdavForm, setWebDAVForm] = useState({
|
||||
@@ -80,6 +87,7 @@ export const SettingsPanel: React.FC = () => {
|
||||
model: '',
|
||||
customPrompt: '',
|
||||
useCustomPrompt: false,
|
||||
concurrency: 1,
|
||||
});
|
||||
setShowAIForm(false);
|
||||
setEditingAIId(null);
|
||||
@@ -113,6 +121,7 @@ export const SettingsPanel: React.FC = () => {
|
||||
isActive: false,
|
||||
customPrompt: aiForm.customPrompt || undefined,
|
||||
useCustomPrompt: aiForm.useCustomPrompt,
|
||||
concurrency: aiForm.concurrency,
|
||||
};
|
||||
|
||||
if (editingAIId) {
|
||||
@@ -132,6 +141,7 @@ export const SettingsPanel: React.FC = () => {
|
||||
model: config.model,
|
||||
customPrompt: config.customPrompt || '',
|
||||
useCustomPrompt: config.useCustomPrompt || false,
|
||||
concurrency: config.concurrency || 1,
|
||||
});
|
||||
setEditingAIId(config.id);
|
||||
setShowAIForm(true);
|
||||
@@ -351,6 +361,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.6', 'Current Version: v0.1.6')}
|
||||
</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 +422,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">
|
||||
@@ -467,6 +535,24 @@ Focus on practicality and accurate categorization to help users quickly understa
|
||||
placeholder="gpt-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('并发数', 'Concurrency')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
value={aiForm.concurrency}
|
||||
onChange={(e) => setAIForm(prev => ({ ...prev, concurrency: Math.max(1, Math.min(10, parseInt(e.target.value) || 1)) }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
|
||||
placeholder="1"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('同时进行AI分析的仓库数量 (1-10)', 'Number of repositories to analyze simultaneously (1-10)')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Prompt Section */}
|
||||
@@ -567,7 +653,7 @@ Focus on practicality and accurate categorization to help users quickly understa
|
||||
)}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{config.baseUrl} • {config.model}
|
||||
{config.baseUrl} • {config.model} • {t('并发数', 'Concurrency')}: {config.concurrency || 1}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -842,4 +928,4 @@ Focus on practicality and accurate categorization to help users quickly understa
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -9,4 +9,14 @@
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.hd-drag {
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.hd-btns {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
@@ -58,8 +58,8 @@ export class AIService {
|
||||
return this.parseAIResponse(content);
|
||||
} catch (error) {
|
||||
console.error('AI analysis failed:', error);
|
||||
// Fallback to basic analysis
|
||||
return this.fallbackAnalysis(repository);
|
||||
// 抛出错误,让调用方处理失败状态
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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.6';
|
||||
}
|
||||
|
||||
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, AnalysisProgress } from '../types';
|
||||
|
||||
interface AppActions {
|
||||
// Auth actions
|
||||
@@ -52,6 +52,13 @@ 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;
|
||||
|
||||
// Update Analysis Progress
|
||||
setAnalysisProgress: (newProgress: AnalysisProgress) => void;
|
||||
}
|
||||
|
||||
const initialSearchFilters: SearchFilters = {
|
||||
@@ -177,6 +184,8 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
theme: 'light',
|
||||
currentView: 'repositories',
|
||||
language: 'zh',
|
||||
updateNotification: null,
|
||||
analysisProgress: { current: 0, total: 0 },
|
||||
|
||||
// Auth actions
|
||||
setUser: (user) => {
|
||||
@@ -307,6 +316,11 @@ 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 }),
|
||||
setAnalysisProgress: (newProgress) => set({ analysisProgress: newProgress })
|
||||
}),
|
||||
{
|
||||
name: 'github-stars-manager',
|
||||
@@ -343,6 +357,12 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
// 持久化UI设置
|
||||
theme: state.theme,
|
||||
language: state.language,
|
||||
|
||||
// 持久化搜索排序设置
|
||||
searchFilters: {
|
||||
sortBy: state.searchFilters.sortBy,
|
||||
sortOrder: state.searchFilters.sortOrder,
|
||||
},
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
@@ -365,8 +385,14 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
// 初始化搜索结果为所有仓库
|
||||
state.searchResults = state.repositories || [];
|
||||
|
||||
// 重置搜索过滤器
|
||||
state.searchFilters = initialSearchFilters;
|
||||
// 重置搜索过滤器,但保留排序设置
|
||||
const savedSortBy = state.searchFilters?.sortBy || 'stars';
|
||||
const savedSortOrder = state.searchFilters?.sortOrder || 'desc';
|
||||
state.searchFilters = {
|
||||
...initialSearchFilters,
|
||||
sortBy: savedSortBy,
|
||||
sortOrder: savedSortOrder,
|
||||
};
|
||||
|
||||
// 确保语言设置存在
|
||||
if (!state.language) {
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface Repository {
|
||||
ai_tags?: string[];
|
||||
ai_platforms?: string[]; // 新增:支持的平台类型
|
||||
analyzed_at?: string;
|
||||
analysis_failed?: boolean; // 新增:AI分析是否失败
|
||||
// Release subscription
|
||||
subscribed_to_releases?: boolean;
|
||||
// Manual editing fields
|
||||
@@ -74,6 +75,7 @@ export interface AIConfig {
|
||||
isActive: boolean;
|
||||
customPrompt?: string; // 自定义提示词
|
||||
useCustomPrompt?: boolean; // 是否使用自定义提示词
|
||||
concurrency?: number; // AI分析并发数,默认为1
|
||||
}
|
||||
|
||||
export interface WebDAVConfig {
|
||||
@@ -152,4 +154,23 @@ export interface AppState {
|
||||
theme: 'light' | 'dark';
|
||||
currentView: 'repositories' | 'releases' | 'settings';
|
||||
language: 'zh' | 'en';
|
||||
|
||||
// Update
|
||||
updateNotification: UpdateNotification | null;
|
||||
|
||||
// Analysis Progress
|
||||
analysisProgress: AnalysisProgress
|
||||
}
|
||||
|
||||
export interface UpdateNotification {
|
||||
version: string;
|
||||
releaseDate: string;
|
||||
changelog: string[];
|
||||
downloadUrl: string;
|
||||
dismissed: boolean;
|
||||
}
|
||||
|
||||
export interface AnalysisProgress {
|
||||
current: number;
|
||||
total: number;
|
||||
}
|
||||
28
test-docker.html
Normal file
28
test-docker.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GitHub Stars Manager Docker Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>GitHub Stars Manager Docker Test</h1>
|
||||
<p>This is a simple test page to verify the Docker deployment.</p>
|
||||
|
||||
<h2>Test Results:</h2>
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
// This script tests that the app is accessible
|
||||
fetch('http://localhost:8080')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
document.getElementById('results').innerHTML = '<p style="color: green;">✓ Application is accessible</p>';
|
||||
} else {
|
||||
document.getElementById('results').innerHTML = '<p style="color: red;">✗ Application is not accessible</p>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('results').innerHTML = '<p style="color: orange;">⚠️ Test cannot be completed from this context: ' + error.message + '</p>';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
107
test-sort-persistence.html
Normal file
107
test-sort-persistence.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<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;
|
||||
}
|
||||
.test-section {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.success {
|
||||
color: #22c55e;
|
||||
font-weight: bold;
|
||||
}
|
||||
.info {
|
||||
color: #3b82f6;
|
||||
}
|
||||
code {
|
||||
background: #e5e7eb;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🎯 仓库排序持久化功能测试</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>✅ 功能实现完成</h2>
|
||||
<p class="success">已成功实现仓库页面排序设置的持久化功能!</p>
|
||||
|
||||
<h3>🔧 实现的修改:</h3>
|
||||
<ul>
|
||||
<li><strong>状态持久化</strong>:在 <code>useAppStore.ts</code> 的 <code>partialize</code> 函数中添加了排序设置的持久化</li>
|
||||
<li><strong>状态恢复</strong>:在 <code>onRehydrateStorage</code> 中保留用户上次设置的排序方式,而不是每次都重置为默认值</li>
|
||||
<li><strong>向下兼容</strong>:如果没有保存的排序设置,会使用默认的"按星标排序"</li>
|
||||
</ul>
|
||||
|
||||
<h3>📋 具体修改内容:</h3>
|
||||
<ol>
|
||||
<li><strong>持久化配置</strong>:
|
||||
<pre><code>// 持久化搜索排序设置
|
||||
searchFilters: {
|
||||
sortBy: state.searchFilters.sortBy,
|
||||
sortOrder: state.searchFilters.sortOrder,
|
||||
},</code></pre>
|
||||
</li>
|
||||
<li><strong>状态恢复逻辑</strong>:
|
||||
<pre><code>// 重置搜索过滤器,但保留排序设置
|
||||
const savedSortBy = state.searchFilters?.sortBy || 'stars';
|
||||
const savedSortOrder = state.searchFilters?.sortOrder || 'desc';
|
||||
state.searchFilters = {
|
||||
...initialSearchFilters,
|
||||
sortBy: savedSortBy,
|
||||
sortOrder: savedSortOrder,
|
||||
};</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🎮 如何测试功能</h2>
|
||||
<ol>
|
||||
<li>启动应用:<code>npm run dev</code></li>
|
||||
<li>进入仓库页面</li>
|
||||
<li>修改排序方式(例如:从"按星标排序"改为"按更新排序")</li>
|
||||
<li>修改排序顺序(点击 ↓/↑ 按钮)</li>
|
||||
<li>刷新页面或重新打开应用</li>
|
||||
<li class="success">✅ 应该看到排序设置被保留了!</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>🚀 功能特点</h2>
|
||||
<ul>
|
||||
<li class="info"><strong>智能持久化</strong>:只保存排序相关设置,其他搜索条件仍然会重置</li>
|
||||
<li class="info"><strong>用户友好</strong>:记住用户的偏好设置,提升使用体验</li>
|
||||
<li class="info"><strong>向下兼容</strong>:对于没有保存设置的用户,使用合理的默认值</li>
|
||||
<li class="info"><strong>轻量级</strong>:只增加了最小必要的存储内容</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>📝 支持的排序选项</h2>
|
||||
<ul>
|
||||
<li><strong>按星标排序</strong> (stars) - 默认选项</li>
|
||||
<li><strong>按更新排序</strong> (updated) - 按最后更新时间</li>
|
||||
<li><strong>按名称排序</strong> (name) - 按仓库名称字母顺序</li>
|
||||
<li><strong>按加星时间排序</strong> (starred) - 按用户加星的时间</li>
|
||||
</ul>
|
||||
<p>每种排序都支持升序 (↑) 和降序 (↓) 两种顺序。</p>
|
||||
</div>
|
||||
|
||||
<p class="success">🎉 功能已完成!现在用户的排序偏好会被记住,不再每次都重置为按星标排序了。</p>
|
||||
</body>
|
||||
</html>
|
||||
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.4</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>
|
||||
BIN
upload/logo.png
Normal file
BIN
upload/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
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. 建议在发布前先在本地测试更新检查功能
|
||||
42
versions/version-info.xml
Normal file
42
versions/version-info.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<versions>
|
||||
<version>
|
||||
<number>0.1.2</number>
|
||||
<releaseDate>2025-08-02</releaseDate>
|
||||
<changelog>
|
||||
<item>The custom filter for releases can now filter out irrelevant downloaded files at the same time.</item>
|
||||
<item>Added sorting by the time a repository is starred.</item>
|
||||
<item>Adjusted repository search: currently, entering a keyword triggers real-time repository name matching, while clicking AI Search will retrieve AI-analyzed content, tags, etc., speeding up search. Natural language search is temporarily disabled due to unsatisfactory results.</item>
|
||||
</changelog>
|
||||
<downloadUrl>https://github.com/AmintaCCCP/GithubStarsManager/releases/tag/v0.1.2-fix</downloadUrl>
|
||||
</version>
|
||||
<version>
|
||||
<number>0.1.3</number>
|
||||
<releaseDate>2025-08-03</releaseDate>
|
||||
<changelog>
|
||||
<item>Add a check for updates feature</item>
|
||||
</changelog>
|
||||
<downloadUrl>https://github.com/AmintaCCCP/GithubStarsManager/releases/tag/v0.1.3</downloadUrl>
|
||||
</version>
|
||||
<version>
|
||||
<number>0.1.4</number>
|
||||
<releaseDate>2025-08-12</releaseDate>
|
||||
<changelog>
|
||||
<item>Persistent sorting for repository list</item>
|
||||
<item>Added AI detection concurrency configuration</item>
|
||||
<item>Unified repository card tag styles</item>
|
||||
<item>Improved AI analysis failure indicators and added a retry button for failed items</item>
|
||||
</changelog>
|
||||
<downloadUrl>https://github.com/AmintaCCCP/GithubStarsManager/releases/tag/v0.1.4</downloadUrl>
|
||||
</version>
|
||||
<version>
|
||||
<number>0.1.5</number>
|
||||
<releaseDate>2025-09-23</releaseDate>
|
||||
<changelog>
|
||||
<item>fix: Fixed the issue where the analysis progress becomes 0 due to switching headers during AI analysis</item>
|
||||
<item>Add drag and no-drag regions for desktop app</item>
|
||||
<item>switch to infinite scroll with consistent card widths</item>
|
||||
</changelog>
|
||||
<downloadUrl>https://github.com/AmintaCCCP/GithubStarsManager/releases/tag/v0.1.5</downloadUrl>
|
||||
</version>
|
||||
</versions>
|
||||
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [react()],
|
||||
optimizeDeps: {
|
||||
exclude: ['lucide-react'],
|
||||
|
||||
Reference in New Issue
Block a user