From 961c1415cbfc7866f795fbba53f30b482917c815 Mon Sep 17 00:00:00 2001 From: tamina Date: Fri, 10 Apr 2026 12:47:24 +0800 Subject: [PATCH 1/2] feat(categories): support hide/delete and drag-to-assign --- src/components/CategorySidebar.tsx | 190 +++++++++++++++++-------- src/components/RepositoryCard.tsx | 24 +++- src/components/RepositoryEditModal.tsx | 10 +- src/components/RepositoryList.tsx | 12 +- src/components/SettingsPanel.tsx | 52 +++++++ src/services/autoSync.ts | 16 +++ src/store/useAppStore.ts | 87 +++++++++-- src/types/index.ts | 2 + src/utils/categoryUtils.ts | 37 +++++ 9 files changed, 350 insertions(+), 80 deletions(-) create mode 100644 src/utils/categoryUtils.ts diff --git a/src/components/CategorySidebar.tsx b/src/components/CategorySidebar.tsx index f85062c..c581d33 100644 --- a/src/components/CategorySidebar.tsx +++ b/src/components/CategorySidebar.tsx @@ -1,25 +1,14 @@ -import React, { useState } from 'react'; -import { - Folder, - Code, - Globe, - Smartphone, - Database, - Shield, - Gamepad2, - Palette, - Bot, - Wrench, - BookOpen, - Zap, - Users, - BarChart3, +import React, { useMemo, useState } from 'react'; +import { Plus, - Edit3 + Edit3, + Trash2, + EyeOff, } from 'lucide-react'; -import { Repository, Category } from '../types'; +import { Category, Repository } from '../types'; import { useAppStore, getAllCategories } from '../store/useAppStore'; import { CategoryEditModal } from './CategoryEditModal'; +import { forceSyncToBackend } from '../services/autoSync'; interface CategorySidebarProps { repositories: Repository[]; @@ -27,7 +16,6 @@ interface CategorySidebarProps { onCategorySelect: (category: string) => void; } - export const CategorySidebar: React.FC = ({ repositories, selectedCategory, @@ -35,37 +23,38 @@ export const CategorySidebar: React.FC = ({ }) => { const { customCategories, + hiddenDefaultCategoryIds, deleteCustomCategory, - language + hideDefaultCategory, + language, + updateRepository, } = useAppStore(); const [editModalOpen, setEditModalOpen] = useState(false); const [editingCategory, setEditingCategory] = useState(null); const [isCreatingCategory, setIsCreatingCategory] = useState(false); + const [dragOverCategoryId, setDragOverCategoryId] = useState(null); - const allCategories = getAllCategories(customCategories, language); + const allCategories = getAllCategories(customCategories, language, hiddenDefaultCategoryIds); + const repositoryMap = useMemo(() => new Map(repositories.map(repo => [String(repo.id), repo])), [repositories]); - // Calculate repository count for each category const getCategoryCount = (category: Category) => { if (category.id === 'all') return repositories.length; - + return repositories.filter(repo => { - // Check custom category first if (repo.custom_category === category.name) { return true; } - - // 优先使用AI标签进行匹配 + if (repo.ai_tags && repo.ai_tags.length > 0) { - return repo.ai_tags.some(tag => - category.keywords.some(keyword => + return repo.ai_tags.some(tag => + category.keywords.some(keyword => tag.toLowerCase().includes(keyword.toLowerCase()) || keyword.toLowerCase().includes(tag.toLowerCase()) ) ); } - - // 如果没有AI标签,使用传统方式匹配 + const repoText = [ repo.name, repo.description || '', @@ -73,8 +62,8 @@ export const CategorySidebar: React.FC = ({ ...(repo.topics || []), repo.ai_summary || '' ].join(' ').toLowerCase(); - - return category.keywords.some(keyword => + + return category.keywords.some(keyword => repoText.includes(keyword.toLowerCase()) ); }).length; @@ -92,12 +81,61 @@ export const CategorySidebar: React.FC = ({ setEditModalOpen(true); }; + const handleDeleteCategory = async (category: Category) => { + const confirmed = confirm( + t( + `确定删除自定义分类“${category.name}”吗?\n\n仓库会保留,Star 不会取消,只会清空它们的手动分类归属。`, + `Delete custom category "${category.name}"?\n\nRepositories will stay starred. Only their manual category assignment will be cleared.` + ) + ); + + if (!confirmed) return; + + deleteCustomCategory(category.id); + await forceSyncToBackend(); + }; + + const handleHideDefaultCategory = async (category: Category) => { + const confirmed = confirm( + t( + `隐藏默认分类“${category.name}”?\n\n这不会删除任何仓库,只是在左侧隐藏这个预设分类。`, + `Hide default category "${category.name}"?\n\nThis will not delete any repositories. It only hides this built-in category from the sidebar.` + ) + ); + + if (!confirmed) return; + + hideDefaultCategory(category.id); + await forceSyncToBackend(); + }; + const handleCloseModal = () => { setEditModalOpen(false); setEditingCategory(null); setIsCreatingCategory(false); }; + const handleDropOnCategory = async (event: React.DragEvent, category: Category) => { + event.preventDefault(); + setDragOverCategoryId(null); + + if (category.id === 'all') return; + + const repoId = event.dataTransfer.getData('application/x-gsm-repository-id'); + const repository = repositoryMap.get(repoId); + if (!repository) return; + + const nextRepo = { + ...repository, + custom_category: category.name, + category_locked: true, + last_edited: new Date().toISOString(), + }; + + updateRepository(nextRepo); + await forceSyncToBackend(); + }; + const t = (zh: string, en: string) => language === 'zh' ? zh : en; return ( @@ -115,21 +153,36 @@ export const CategorySidebar: React.FC = ({ - +
{allCategories.map(category => { const count = getCategoryCount(category); const isSelected = selectedCategory === category.id; - + const isDragTarget = dragOverCategoryId === category.id; + return (
- )} -
+ {count}
+ + {category.id !== 'all' && ( + + )} + + {category.id !== 'all' && category.isCustom && ( + + )} + + {category.id !== 'all' && !category.isCustom && ( + + )} + ); })} - - {/* Category Edit Modal */} + = ({ setLoading, language, customCategories, + hiddenDefaultCategoryIds, updateRepository, deleteRepository } = useAppStore(); @@ -39,6 +41,7 @@ export const RepositoryCard: React.FC = ({ const [unstarring, setUnstarring] = useState(false); const descriptionRef = useRef(null); + const allCategories = getAllCategories(customCategories, language, hiddenDefaultCategoryIds); const isSubscribed = releaseSubscriptions.has(repository.id); @@ -188,11 +191,12 @@ export const RepositoryCard: React.FC = ({ const [owner, name] = repository.full_name.split('/'); const readmeContent = await githubApi.getRepositoryReadme(owner, name); - // 获取自定义分类名称列表 - const customCategoryNames = customCategories.map(cat => cat.name); + // 获取可用分类名称列表 + const categoryNames = allCategories.filter(cat => cat.id !== 'all').map(cat => cat.name); // AI分析 - const analysis = await aiService.analyzeRepository(repository, readmeContent, customCategoryNames); + const analysis = await aiService.analyzeRepository(repository, readmeContent, categoryNames); + const resolvedCategory = resolveCategoryAssignment(repository, analysis.tags, allCategories); // 更新仓库信息 const updatedRepo = { @@ -200,6 +204,7 @@ export const RepositoryCard: React.FC = ({ ai_summary: analysis.summary, ai_tags: analysis.tags, ai_platforms: analysis.platforms, + custom_category: resolvedCategory, analyzed_at: new Date().toISOString(), analysis_failed: false // 分析成功,清除失败标记 }; @@ -350,8 +355,17 @@ export const RepositoryCard: React.FC = ({ } }; + const handleDragStart = (event: React.DragEvent) => { + event.dataTransfer.setData('application/x-gsm-repository-id', String(repository.id)); + event.dataTransfer.effectAllowed = 'move'; + }; + return ( -
+
{/* Header - Repository Info */}
= ({ onClose, repository }) => { - const { updateRepository, language, customCategories, repositories } = useAppStore(); + const { updateRepository, language, customCategories, hiddenDefaultCategoryIds } = useAppStore(); const [formData, setFormData] = useState({ description: '', @@ -25,7 +26,7 @@ export const RepositoryEditModal: React.FC = ({ }); const [newTag, setNewTag] = useState(''); - const allCategories = getAllCategories(customCategories, language); + const allCategories = getAllCategories(customCategories, language, hiddenDefaultCategoryIds); // 获取仓库当前所属的分类 const getCurrentCategory = (repo: Repository) => { @@ -83,7 +84,7 @@ export const RepositoryEditModal: React.FC = ({ } }, [repository, isOpen]); - const handleSave = () => { + const handleSave = async () => { if (!repository) return; const originalCategory = getCurrentCategory(repository); @@ -100,6 +101,7 @@ export const RepositoryEditModal: React.FC = ({ }; updateRepository(updatedRepo); + await forceSyncToBackend(); onClose(); }; @@ -298,7 +300,7 @@ export const RepositoryEditModal: React.FC = ({ {t('取消', 'Cancel')}
+ {/* Language Settings */} +
+
+ +

+ {t('分类显示管理', 'Category Visibility')} +

+
+ + {hiddenDefaultCategoryIds.length === 0 ? ( +

+ {t('当前没有隐藏的默认分类。', 'No default categories are hidden right now.')} +

+ ) : ( +
+

+ {t('以下默认分类已被隐藏,你可以在这里恢复显示。', 'The following built-in categories are hidden. You can restore them here.')} +

+
+ {hiddenDefaultCategoryIds.map((categoryId) => ( + + ))} +
+
+ )} +
+ {/* Contact Information */}
diff --git a/src/services/autoSync.ts b/src/services/autoSync.ts index e046d06..ee7b2dc 100644 --- a/src/services/autoSync.ts +++ b/src/services/autoSync.ts @@ -149,6 +149,20 @@ export async function syncFromBackend(): Promise { if (typeof settings.activeWebDAVConfig === 'string' || settings.activeWebDAVConfig === null) { state.setActiveWebDAVConfig(settings.activeWebDAVConfig as string | null); } + if (Array.isArray(settings.hiddenDefaultCategoryIds)) { + const nextHiddenIds = settings.hiddenDefaultCategoryIds.filter((id): id is string => typeof id === 'string'); + const currentHiddenIds = state.hiddenDefaultCategoryIds || []; + for (const id of currentHiddenIds) { + if (!nextHiddenIds.includes(id)) { + state.showDefaultCategory(id); + } + } + for (const id of nextHiddenIds) { + if (!currentHiddenIds.includes(id)) { + state.hideDefaultCategory(id); + } + } + } _lastHash.settings = hashes.settings; } @@ -195,6 +209,7 @@ export async function syncToBackend(): Promise { backend.syncSettings({ activeAIConfig: state.activeAIConfig, activeWebDAVConfig: state.activeWebDAVConfig, + hiddenDefaultCategoryIds: state.hiddenDefaultCategoryIds, }), ]); const [reposSync, releasesSync, aiSync, webdavSync, settingsSync] = results; @@ -217,6 +232,7 @@ export async function syncToBackend(): Promise { _lastHash.settings = quickHash({ activeAIConfig: state.activeAIConfig, activeWebDAVConfig: state.activeWebDAVConfig, + hiddenDefaultCategoryIds: state.hiddenDefaultCategoryIds, }); } } catch (err) { diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 9a41d2f..ae7ab58 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -62,6 +62,8 @@ interface AppActions { addCustomCategory: (category: Category) => void; updateCustomCategory: (id: string, updates: Partial) => void; deleteCustomCategory: (id: string) => void; + hideDefaultCategory: (id: string) => void; + showDefaultCategory: (id: string) => void; // Asset Filter actions addAssetFilter: (filter: AssetFilter) => void; @@ -111,6 +113,7 @@ type PersistedAppState = Partial< | 'lastBackup' | 'releases' | 'customCategories' + | 'hiddenDefaultCategoryIds' | 'assetFilters' | 'theme' | 'currentView' @@ -162,6 +165,7 @@ const normalizePersistedState = ( }, webdavConfigs: Array.isArray(safePersisted.webdavConfigs) ? safePersisted.webdavConfigs : [], customCategories: Array.isArray(safePersisted.customCategories) ? safePersisted.customCategories : [], + hiddenDefaultCategoryIds: Array.isArray((safePersisted as any).hiddenDefaultCategoryIds) ? (safePersisted as any).hiddenDefaultCategoryIds.filter((id: unknown): id is string => typeof id === 'string') : [], assetFilters: Array.isArray(safePersisted.assetFilters) ? safePersisted.assetFilters : [], language: safePersisted.language || 'zh', isAuthenticated: !!(safePersisted.user && safePersisted.githubToken), @@ -276,6 +280,7 @@ export const useAppStore = create()( releaseSubscriptions: new Set(), readReleases: new Set(), customCategories: [], + hiddenDefaultCategoryIds: [], assetFilters: [], theme: 'light', currentView: 'repositories', @@ -408,13 +413,68 @@ export const useAppStore = create()( addCustomCategory: (category) => set((state) => ({ customCategories: [...state.customCategories, { ...category, isCustom: true }] })), - updateCustomCategory: (id, updates) => set((state) => ({ - customCategories: state.customCategories.map(category => + updateCustomCategory: (id, updates) => set((state) => { + const targetCategory = state.customCategories.find(category => category.id === id); + const nextCategories = state.customCategories.map(category => category.id === id ? { ...category, ...updates } : category - ) + ); + + if (!targetCategory || !updates.name || updates.name === targetCategory.name) { + return { customCategories: nextCategories }; + } + + const nextRepositories = state.repositories.map(repo => + repo.custom_category === targetCategory.name + ? { ...repo, custom_category: updates.name, last_edited: new Date().toISOString() } + : repo + ); + + return { + customCategories: nextCategories, + repositories: nextRepositories, + searchResults: state.searchResults.map(repo => + repo.custom_category === targetCategory.name + ? { ...repo, custom_category: updates.name, last_edited: new Date().toISOString() } + : repo + ) + }; + }), + deleteCustomCategory: (id) => set((state) => { + const targetCategory = state.customCategories.find(category => category.id === id); + const nextSelectedCategory = state.selectedCategory === id ? 'all' : state.selectedCategory; + + if (!targetCategory) { + return { + customCategories: state.customCategories.filter(category => category.id !== id), + selectedCategory: nextSelectedCategory + }; + } + + const clearedRepositories = state.repositories.map(repo => + repo.custom_category === targetCategory.name + ? { ...repo, custom_category: undefined, last_edited: new Date().toISOString() } + : repo + ); + + return { + customCategories: state.customCategories.filter(category => category.id !== id), + repositories: clearedRepositories, + searchResults: state.searchResults.map(repo => + repo.custom_category === targetCategory.name + ? { ...repo, custom_category: undefined, last_edited: new Date().toISOString() } + : repo + ), + selectedCategory: nextSelectedCategory + }; + }), + hideDefaultCategory: (id) => set((state) => ({ + hiddenDefaultCategoryIds: state.hiddenDefaultCategoryIds.includes(id) + ? state.hiddenDefaultCategoryIds + : [...state.hiddenDefaultCategoryIds, id], + selectedCategory: state.selectedCategory === id ? 'all' : state.selectedCategory })), - deleteCustomCategory: (id) => set((state) => ({ - customCategories: state.customCategories.filter(category => category.id !== id) + showDefaultCategory: (id) => set((state) => ({ + hiddenDefaultCategoryIds: state.hiddenDefaultCategoryIds.filter(categoryId => categoryId !== id) })), // Asset Filter actions @@ -475,6 +535,7 @@ export const useAppStore = create()( // 持久化自定义分类 customCategories: state.customCategories, + hiddenDefaultCategoryIds: state.hiddenDefaultCategoryIds, // 持久化资源过滤器 assetFilters: state.assetFilters, @@ -519,11 +580,17 @@ export const useAppStore = create()( ); // Helper function to get all categories (default + custom) -export const getAllCategories = (customCategories: Category[], language: 'zh' | 'en' = 'zh'): Category[] => { - const translatedDefaults = defaultCategories.map(cat => ({ - ...cat, - name: language === 'en' ? translateCategoryName(cat.name) : cat.name - })); +export const getAllCategories = ( + customCategories: Category[], + language: 'zh' | 'en' = 'zh', + hiddenDefaultCategoryIds: string[] = [] +): Category[] => { + const translatedDefaults = defaultCategories + .filter(cat => !hiddenDefaultCategoryIds.includes(cat.id)) + .map(cat => ({ + ...cat, + name: language === 'en' ? translateCategoryName(cat.name) : cat.name + })); return [...translatedDefaults, ...customCategories]; }; diff --git a/src/types/index.ts b/src/types/index.ts index 01bd66d..c44ad61 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -117,6 +117,7 @@ export interface Category { icon: string; keywords: string[]; isCustom?: boolean; + isHidden?: boolean; } export interface AssetFilter { @@ -156,6 +157,7 @@ export interface AppState { // Categories customCategories: Category[]; // 新增:自定义分类 + hiddenDefaultCategoryIds: string[]; // Asset Filters assetFilters: AssetFilter[]; // 新增:资源过滤器 diff --git a/src/utils/categoryUtils.ts b/src/utils/categoryUtils.ts new file mode 100644 index 0000000..287b0c3 --- /dev/null +++ b/src/utils/categoryUtils.ts @@ -0,0 +1,37 @@ +import { Category, Repository } from '../types'; + +export const resolveCategoryAssignment = ( + repository: Repository, + aiTags: string[] | undefined, + allCategories: Category[] +): string | undefined => { + if (repository.category_locked && repository.custom_category) { + return repository.custom_category; + } + + const normalizedTags = Array.isArray(aiTags) ? aiTags.filter(Boolean) : []; + if (normalizedTags.length === 0) { + return repository.category_locked ? repository.custom_category : undefined; + } + + const customCategories = allCategories.filter(category => category.id !== 'all' && category.isCustom); + const defaultCategories = allCategories.filter(category => category.id !== 'all' && !category.isCustom); + + const matchCategory = (categories: Category[]) => categories.find(category => + normalizedTags.some(tag => + category.name.toLowerCase() === tag.toLowerCase() || + category.keywords.some(keyword => + tag.toLowerCase().includes(keyword.toLowerCase()) || + keyword.toLowerCase().includes(tag.toLowerCase()) + ) + ) + ); + + const customMatch = matchCategory(customCategories); + if (customMatch) return customMatch.name; + + const defaultMatch = matchCategory(defaultCategories); + if (defaultMatch) return undefined; + + return repository.category_locked ? repository.custom_category : undefined; +}; From ed78d40203085bd2dfd0e00dbf600960e121f89c Mon Sep 17 00:00:00 2001 From: tamina Date: Fri, 10 Apr 2026 17:27:48 +0800 Subject: [PATCH 2/2] fix(persistence): backend support for category_locked and settings sync refinements --- server/src/db/schema.ts | 2 ++ server/src/routes/repositories.ts | 8 +++++--- server/src/routes/sync.ts | 6 +++--- src/components/SettingsPanel.tsx | 23 ++++++++++++++++++----- src/store/useAppStore.ts | 4 ++-- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index 058e0dc..f69c79f 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -37,6 +37,7 @@ export function initializeSchema(db: Database.Database): void { custom_description TEXT, custom_tags TEXT, custom_category TEXT, + category_locked INTEGER DEFAULT 0, last_edited TEXT, subscribed_to_releases INTEGER DEFAULT 0 ); @@ -102,4 +103,5 @@ export function initializeSchema(db: Database.Database): void { `); addColumnIfMissing(db, 'ai_configs', 'reasoning_effort', 'TEXT'); + addColumnIfMissing(db, 'repositories', 'category_locked', 'INTEGER DEFAULT 0'); } diff --git a/server/src/routes/repositories.ts b/server/src/routes/repositories.ts index b47f49f..e65672b 100644 --- a/server/src/routes/repositories.ts +++ b/server/src/routes/repositories.ts @@ -36,6 +36,7 @@ function transformRepo(row: Record) { custom_description: row.custom_description, custom_tags: parseJsonColumn(row.custom_tags), custom_category: row.custom_category, + category_locked: !!row.category_locked, last_edited: row.last_edited, subscribed_to_releases: !!row.subscribed_to_releases, }; @@ -95,9 +96,9 @@ router.put('/api/repositories', (req, res) => { created_at, updated_at, pushed_at, starred_at, owner_login, owner_avatar_url, topics, ai_summary, ai_tags, ai_platforms, analyzed_at, analysis_failed, - custom_description, custom_tags, custom_category, last_edited, + custom_description, custom_tags, custom_category, category_locked, last_edited, subscribed_to_releases - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const deleteAllReleases = db.prepare('DELETE FROM releases'); @@ -142,7 +143,7 @@ router.put('/api/repositories', (req, res) => { repo.analyzed_at ?? null, (repo.analysis_failed === true || repo.analysis_failed === 1) ? 1 : 0, repo.custom_description ?? null, JSON.stringify(Array.isArray(repo.custom_tags) ? repo.custom_tags : []), - repo.custom_category ?? null, repo.last_edited ?? null, + repo.custom_category ?? null, (repo.category_locked === true || repo.category_locked === 1) ? 1 : 0, repo.last_edited ?? null, (repo.subscribed_to_releases === true || repo.subscribed_to_releases === 1) ? 1 : 0 ); count++; @@ -174,6 +175,7 @@ router.patch('/api/repositories/:id', (req, res) => { custom_description: (v) => v, custom_tags: (v) => JSON.stringify(Array.isArray(v) ? v : []), custom_category: (v) => v, + category_locked: (v) => (v === true || v === 1) ? 1 : 0, last_edited: (v) => v, subscribed_to_releases: (v) => (v === true || v === 1) ? 1 : 0, description: (v) => v, diff --git a/server/src/routes/sync.ts b/server/src/routes/sync.ts index c0d4709..c733bc1 100644 --- a/server/src/routes/sync.ts +++ b/server/src/routes/sync.ts @@ -101,9 +101,9 @@ router.post('/api/sync/import', (req, res) => { created_at, updated_at, pushed_at, starred_at, owner_login, owner_avatar_url, topics, ai_summary, ai_tags, ai_platforms, analyzed_at, analysis_failed, - custom_description, custom_tags, custom_category, last_edited, + custom_description, custom_tags, custom_category, category_locked, last_edited, subscribed_to_releases - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); for (const r of repos) { repoStmt.run( @@ -119,7 +119,7 @@ router.post('/api/sync/import', (req, res) => { r.analyzed_at ?? null, r.analysis_failed ? 1 : 0, r.custom_description ?? null, typeof r.custom_tags === 'string' ? r.custom_tags : JSON.stringify(r.custom_tags ?? []), - r.custom_category ?? null, r.last_edited ?? null, + r.custom_category ?? null, (r.category_locked === true || r.category_locked === 1) ? 1 : 0, r.last_edited ?? null, r.subscribed_to_releases ? 1 : 0 ); } diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index 4eab235..cad8b84 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -23,7 +23,7 @@ import { Server, } from 'lucide-react'; import { AIConfig, WebDAVConfig, AIApiType, AIReasoningEffort } from '../types'; -import { useAppStore } from '../store/useAppStore'; +import { useAppStore, getAllCategories } from '../store/useAppStore'; import { AIService } from '../services/aiService'; import { WebDAVService } from '../services/webdavService'; import { UpdateChecker } from './UpdateChecker'; @@ -522,6 +522,8 @@ Focus on practicality and accurate categorization to help users quickly understa }; const t = (zh: string, en: string) => language === 'zh' ? zh : en; + const hiddenDefaultCategories = getAllCategories([], language, []).filter(category => hiddenDefaultCategoryIds.includes(category.id)); + const handleTestBackendConnection = async () => { setBackendStatus('checking'); @@ -586,6 +588,7 @@ Focus on practicality and accurate categorization to help users quickly understa const releaseData = await backend.fetchReleases(); const aiConfigData = await backend.fetchAIConfigs(); const webdavConfigData = await backend.fetchWebDAVConfigs(); + const settingsData = await backend.fetchSettings(); if (repoData.repositories.length > 0) { setRepositories(repoData.repositories); @@ -599,6 +602,16 @@ Focus on practicality and accurate categorization to help users quickly understa if (webdavConfigData.length > 0) { setWebDAVConfigs(webdavConfigData); } + if (Array.isArray(hiddenDefaultCategoryIds)) { + for (const categoryId of hiddenDefaultCategoryIds) { + if (typeof categoryId === 'string') showDefaultCategory(categoryId); + } + } + if (Array.isArray(settingsData.hiddenDefaultCategoryIds)) { + for (const categoryId of settingsData.hiddenDefaultCategoryIds) { + if (typeof categoryId === 'string') hideDefaultCategory(categoryId); + } + } alert(t( `已从后端同步:仓库 ${repoData.repositories.length},发布 ${releaseData.releases.length},AI配置 ${aiConfigData.length},WebDAV配置 ${webdavConfigData.length}`, @@ -695,13 +708,13 @@ Focus on practicality and accurate categorization to help users quickly understa {t('以下默认分类已被隐藏,你可以在这里恢复显示。', 'The following built-in categories are hidden. You can restore them here.')}

- {hiddenDefaultCategoryIds.map((categoryId) => ( + {hiddenDefaultCategories.map((category) => ( ))}
diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index ae7ab58..54112ec 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -452,7 +452,7 @@ export const useAppStore = create()( const clearedRepositories = state.repositories.map(repo => repo.custom_category === targetCategory.name - ? { ...repo, custom_category: undefined, last_edited: new Date().toISOString() } + ? { ...repo, custom_category: undefined, category_locked: false, last_edited: new Date().toISOString() } : repo ); @@ -461,7 +461,7 @@ export const useAppStore = create()( repositories: clearedRepositories, searchResults: state.searchResults.map(repo => repo.custom_category === targetCategory.name - ? { ...repo, custom_category: undefined, last_edited: new Date().toISOString() } + ? { ...repo, custom_category: undefined, category_locked: false, last_edited: new Date().toISOString() } : repo ), selectedCategory: nextSelectedCategory