diff --git a/package-lock.json b/package-lock.json index 6651aba..e05aaec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "github-stars-manager", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "github-stars-manager", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "date-fns": "^3.3.1", "lucide-react": "^0.344.0", diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index fa21252..2c48dfa 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -61,8 +61,11 @@ export function initializeSchema(db: Database.Database): void { CREATE TABLE IF NOT EXISTS categories ( id TEXT PRIMARY KEY, name TEXT NOT NULL, + description TEXT, icon TEXT NOT NULL DEFAULT '📁', keywords TEXT, + color TEXT, + sort_order INTEGER DEFAULT 0, is_custom INTEGER DEFAULT 1 ); @@ -93,7 +96,10 @@ export function initializeSchema(db: Database.Database): void { CREATE TABLE IF NOT EXISTS asset_filters ( id TEXT PRIMARY KEY, name TEXT NOT NULL, - keywords TEXT + description TEXT, + keywords TEXT, + platform TEXT, + sort_order INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS settings ( @@ -106,4 +112,10 @@ export function initializeSchema(db: Database.Database): void { addColumnIfMissing(db, 'repositories', 'category_locked', 'INTEGER DEFAULT 0'); addColumnIfMissing(db, 'releases', 'zipball_url', 'TEXT'); addColumnIfMissing(db, 'releases', 'tarball_url', 'TEXT'); + addColumnIfMissing(db, 'categories', 'description', 'TEXT'); + addColumnIfMissing(db, 'categories', 'color', 'TEXT'); + addColumnIfMissing(db, 'categories', 'sort_order', 'INTEGER DEFAULT 0'); + addColumnIfMissing(db, 'asset_filters', 'description', 'TEXT'); + addColumnIfMissing(db, 'asset_filters', 'platform', 'TEXT'); + addColumnIfMissing(db, 'asset_filters', 'sort_order', 'INTEGER DEFAULT 0'); } diff --git a/server/src/routes/proxy.ts b/server/src/routes/proxy.ts index 4f469e0..c37a516 100644 --- a/server/src/routes/proxy.ts +++ b/server/src/routes/proxy.ts @@ -15,6 +15,19 @@ function buildApiUrl(baseUrl: string, pathWithVersion: string): string { const base = new URL(baseUrlWithSlash); const basePath = base.pathname.replace(/\/$/, ''); + // 检测 baseUrl 是否已经以任何版本号结尾(v1, v2, v3, v1beta, v1alpha 等) + // 这样可以兼容火山引擎(/v3)、OpenAI(/v1)、Gemini(/v1beta)等不同版本号 + const anyVersionPattern = /\/v\d+(?:beta|alpha)?$/; + const hasVersionInBase = anyVersionPattern.test(basePath); + + if (hasVersionInBase) { + // baseUrl 已包含版本号,只补全端点路径(去掉版本号部分) + const endpointPath = pathWithVersion.includes('/') + ? pathWithVersion.split('/').slice(1).join('/') + : pathWithVersion; + return new URL(endpointPath, baseUrlWithSlash).toString(); + } + if (versionPrefix) { const versionRe = new RegExp(`/${versionPrefix}$`); if (versionRe.test(basePath) && pathWithVersion.startsWith(`${versionPrefix}/`)) { diff --git a/server/src/routes/releases.ts b/server/src/routes/releases.ts index 7cdefb6..316af5a 100644 --- a/server/src/routes/releases.ts +++ b/server/src/routes/releases.ts @@ -175,4 +175,29 @@ router.post('/api/releases/mark-all-read', (_req, res) => { } }); +// DELETE /api/releases/:id +router.delete('/api/releases/:id', (req, res) => { + try { + const db = getDb(); + const id = parseInt(req.params.id); + + if (isNaN(id) || id <= 0) { + res.status(400).json({ error: 'Valid release id required', code: 'INVALID_RELEASE_ID' }); + return; + } + + const result = db.prepare('DELETE FROM releases WHERE id = ?').run(id); + + if (result.changes === 0) { + res.status(404).json({ error: 'Release not found', code: 'RELEASE_NOT_FOUND' }); + return; + } + + res.json({ deleted: true, id }); + } catch (err) { + console.error('DELETE /api/releases/:id error:', err); + res.status(500).json({ error: 'Failed to delete release', code: 'DELETE_RELEASE_FAILED' }); + } +}); + export default router; diff --git a/server/src/routes/repositories.ts b/server/src/routes/repositories.ts index a297acf..a7362a6 100644 --- a/server/src/routes/repositories.ts +++ b/server/src/routes/repositories.ts @@ -241,4 +241,45 @@ router.patch('/api/repositories/:id', (req, res) => { } }); +// DELETE /api/repositories/:id +router.delete('/api/repositories/:id', (req, res) => { + try { + const db = getDb(); + const id = parseInt(req.params.id); + + if (isNaN(id) || id <= 0) { + res.status(400).json({ error: 'Valid repository id required', code: 'INVALID_REPOSITORY_ID' }); + return; + } + + const deleteReleases = db.prepare('DELETE FROM releases WHERE repo_id = ?'); + const deleteRepo = db.prepare('DELETE FROM repositories WHERE id = ?'); + + const deleteAll = db.transaction(() => { + const releaseResult = deleteReleases.run(id); + const repoResult = deleteRepo.run(id); + + if (repoResult.changes === 0) { + throw new Error('Repository not found or already deleted'); + } + + return { + releasesDeleted: releaseResult.changes, + repoDeleted: repoResult.changes + }; + }); + + const result = deleteAll(); + + res.json({ + deleted: true, + id, + releasesDeleted: result.releasesDeleted + }); + } catch (err) { + console.error('DELETE /api/repositories/:id error:', err); + res.status(500).json({ error: 'Failed to delete repository', code: 'DELETE_REPOSITORY_FAILED' }); + } +}); + export default router; diff --git a/server/src/types/api.ts b/server/src/types/api.ts new file mode 100644 index 0000000..9c22cc5 --- /dev/null +++ b/server/src/types/api.ts @@ -0,0 +1,155 @@ +export interface RepositoryRow { + id: number; + name: string; + full_name: string; + description: string | null; + html_url: string; + stargazers_count: number; + language: string | null; + created_at: string | null; + updated_at: string | null; + pushed_at: string | null; + starred_at: string | null; + owner_login: string; + owner_avatar_url: string | null; + topics: string; + ai_summary: string | null; + ai_tags: string; + ai_platforms: string; + analyzed_at: string | null; + analysis_failed: number; + custom_description: string | null; + custom_tags: string; + custom_category: string | null; + category_locked: number; + last_edited: string | null; + subscribed_to_releases: number; +} + +export interface ReleaseRow { + id: number; + tag_name: string; + name: string | null; + body: string | null; + published_at: string | null; + html_url: string | null; + assets: string; + repo_id: number; + repo_full_name: string; + repo_name: string; + prerelease: number; + draft: number; + is_read: number; + zipball_url: string | null; + tarball_url: string | null; +} + +export interface CategoryRow { + id: string; + name: string; + description: string | null; + icon: string; + keywords: string; + color: string | null; + sort_order: number; + is_custom: number; +} + +export interface AIConfigRow { + id: string; + name: string; + api_type: string; + base_url: string; + api_key_encrypted: string; + model: string; + is_active: number; + custom_prompt: string | null; + use_custom_prompt: number; + concurrency: number; + reasoning_effort: string | null; +} + +export interface WebDAVConfigRow { + id: string; + name: string; + url: string; + username: string; + password_encrypted: string; + path: string; + is_active: number; +} + +export interface AssetFilterRow { + id: string; + name: string; + description: string | null; + keywords: string; + platform: string | null; + sort_order: number; +} + +export interface SettingsRow { + key: string; + value: string | null; +} + +export interface ApiResponse { + data?: T; + error?: string; + code?: string; +} + +export interface PaginatedResponse { + items: T[]; + total: number; + page: number; + limit: number; +} + +export interface SyncRepositoriesRequest { + repositories: Record[]; + isFullSync?: boolean; +} + +export interface SyncReleasesRequest { + releases: Record[]; +} + +export interface SyncAIConfigsRequest { + configs: Array<{ + id: string; + name: string; + apiType?: string; + baseUrl: string; + apiKey: string; + model: string; + isActive: boolean; + customPrompt?: string; + useCustomPrompt?: boolean; + concurrency?: number; + reasoningEffort?: string; + }>; +} + +export interface SyncWebDAVConfigsRequest { + configs: Array<{ + id: string; + name: string; + url: string; + username: string; + password: string; + path: string; + isActive: boolean; + }>; +} + +export interface SyncSettingsRequest { + activeAIConfig?: string | null; + activeWebDAVConfig?: string | null; + hiddenDefaultCategoryIds?: string[]; + categoryOrder?: string[]; + customCategories?: unknown[]; + assetFilters?: unknown[]; + collapsedSidebarCategoryCount?: number; + github_token?: string; +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 7a2b5aa..9a138a8 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { Settings, Calendar, Search, Moon, Sun, LogOut, RefreshCw } from 'lucide-react'; +import React, { useState, useRef, useEffect } from 'react'; +import { Settings, Calendar, Search, Moon, Sun, LogOut, RefreshCw, Menu, X } from 'lucide-react'; import { useAppStore } from '../store/useAppStore'; import { GitHubApiService } from '../services/githubApi'; @@ -22,6 +22,45 @@ export const Header: React.FC = () => { language, } = useAppStore(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [isTextWrapped, setIsTextWrapped] = useState(false); + const navRef = useRef(null); + + useEffect(() => { + const checkIfTextWrapped = () => { + const windowWidth = window.innerWidth; + if (windowWidth < 1100) { + setIsTextWrapped(true); + return; + } + + if (navRef.current) { + const buttons = navRef.current.querySelectorAll('button'); + let wrapped = false; + buttons.forEach(button => { + if (button.scrollHeight > button.clientHeight + 5) { + wrapped = true; + } + }); + setIsTextWrapped(wrapped); + } + }; + + checkIfTextWrapped(); + + const resizeObserver = new ResizeObserver(checkIfTextWrapped); + if (navRef.current) { + resizeObserver.observe(navRef.current); + } + + window.addEventListener('resize', checkIfTextWrapped); + + return () => { + resizeObserver.disconnect(); + window.removeEventListener('resize', checkIfTextWrapped); + }; + }, []); + const handleSync = async () => { if (!githubToken) { alert('GitHub token not found. Please login again.'); @@ -103,110 +142,193 @@ export const Header: React.FC = () => { const t = (zh: string, en: string) => language === 'zh' ? zh : en; + + return ( -
+
-
+
{/* Logo and Title */} -
-
-
- GitHub Stars Manager -
-
-

- GitHub Stars Manager -

-

- AI-powered repository management -

-
+
+
+ GitHub Stars Manager +
+
+

+ GitHub Stars Manager +

+

+ AI-powered repository management +

+
+
+

+ GitHub Stars +

-
- {/* Navigation */} -