feat: 多语言按钮支持/重构设置界面/新增数据管理/优化侧边栏/新增返回顶部按钮#81
feat: 多语言按钮支持/重构设置界面/新增数据管理/优化侧边栏/新增返回顶部按钮#81SummerRay160 wants to merge 19 commits intoAmintaCCCP:mainfrom
Conversation
为订阅和查看GitHub按钮添加中文标题支持,根据当前语言环境显示相应文本
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a tabbed SettingsPanel with multiple new settings subpanels; implements repository multi-select and bulk actions with toolbar/modals; introduces README modal and Markdown renderer; adds BackToTop and responsive/collapsible CategorySidebar with persisted ordering/collapse state and shortcut; adds WebDAV/backup/restore, backend sync, preset asset-filters, store/type additions, CSS utilities, and new UI components. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant BackendPanel
participant BackendService
participant AppStore
participant Alert
User->>BackendPanel: Click "Test Connection" / "Sync to Backend" / "Sync from Backend"
BackendPanel->>AppStore: setBackendApiSecret (if provided)
BackendPanel->>BackendService: init()
BackendService-->>BackendPanel: init result
BackendPanel->>BackendService: checkHealth() / verifyAuth()
BackendService-->>BackendPanel: health/auth result
BackendPanel->>BackendService: syncToBackend(data) / fetchFromBackend()
BackendService-->>BackendPanel: sync/fetch result
BackendPanel->>AppStore: apply fetched data (when present)
BackendPanel->>Alert: show success/failure
sequenceDiagram
participant User
participant BackupPanel
participant WebDAVService
participant AppStore
participant Alert
User->>BackupPanel: Click "Backup"
BackupPanel->>AppStore: gather data (repos, releases, configs, categories)
BackupPanel->>WebDAVService: uploadBackup(jsonPayload)
WebDAVService-->>BackupPanel: upload result
BackupPanel->>AppStore: set lastBackup (on success)
BackupPanel->>Alert: show success/failure
User->>BackupPanel: Click "Restore"
BackupPanel->>WebDAVService: list backups -> download latest
WebDAVService-->>BackupPanel: backup JSON
BackupPanel->>AppStore: restore repos/releases/configs/categories (preserve masked secrets)
BackupPanel->>Alert: summary success/failure
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
新增设置模块的多个面板组件,包括通用设置、WebDAV配置、备份恢复、分类管理、后端服务和AI配置。每个面板提供相应的功能界面和交互逻辑,支持多语言切换。 - 通用设置面板支持语言切换和版本检查 - WebDAV面板提供配置管理和测试功能 - 备份面板支持数据备份和恢复操作 - 分类面板管理自定义分类和默认分类显示 - 后端面板处理服务器连接和数据同步 - AI面板配置AI服务参数和测试连接
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (8)
src/components/settings/GeneralPanel.tsx (1)
81-81: Hardcoded version string should be centralized.The version
v0.3.0is hardcoded in the translation string. This will likely get out of sync with actual releases. Consider importing from a central constant or package.json.♻️ Proposed centralized version approach
+// At the top of the file or in a constants file: +const APP_VERSION = import.meta.env.VITE_APP_VERSION || 'v0.3.0'; // In the component: - <p className="text-sm text-gray-600 dark:text-gray-400 mb-1"> - {t('当前版本: v0.3.0', 'Current Version: v0.3.0')} - </p> + <p className="text-sm text-gray-600 dark:text-gray-400 mb-1"> + {t(`当前版本: ${APP_VERSION}`, `Current Version: ${APP_VERSION}`)} + </p>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/GeneralPanel.tsx` at line 81, Replace the hardcoded "v0.3.0" in the GeneralPanel JSX translation call by sourcing the app version from a single source of truth (e.g., import VERSION from package.json or a central constants module) and pass it into the translation (use a translation key with a placeholder or build the localized string dynamically) so the displayed version is always the imported VERSION; update the call site where t(...) is used in GeneralPanel to use that imported VERSION value instead of the literal string.src/components/settings/CategoryPanel.tsx (2)
41-46: ID generation withDate.now()could cause collisions.Using
Date.now()for ID generation is simple but could produce duplicates if a user rapidly adds multiple categories. Consider using a more robust ID generation approach.♻️ Use crypto.randomUUID() for robust ID generation
const newCategory = { - id: `custom-${Date.now()}`, + id: `custom-${crypto.randomUUID()}`, name: newCategoryName.trim(), icon: newCategoryIcon, isCustom: true, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/CategoryPanel.tsx` around lines 41 - 46, The newCategory object uses id: `custom-${Date.now()}` which can collide; update the ID generation in the CategoryPanel component to use a robust UUID (e.g., call crypto.randomUUID()) instead of Date.now() when constructing newCategory (referencing the newCategory variable and newCategoryName/newCategoryIcon fields), and add a small fallback (e.g., fallback to a random string) if crypto.randomUUID is undefined to preserve compatibility.
54-58: Type mismatch inhandleStartEditparameter.The function parameter uses
{ id: string; name: string; icon: string }but based on the context snippet, the category objects fromgetAllCategorieshave alabelfield instead ofname. This may causecategory.nameto beundefinedwhen editing default categories (though this function is only called for custom categories which do havename).The current code works because it's only invoked for
customCategories, but the type annotation is misleading.♻️ Clarify type or use existing Category type
-const handleStartEdit = (category: { id: string; name: string; icon: string }) => { +const handleStartEdit = (category: typeof customCategories[number]) => { setEditingId(category.id); setEditName(category.name); setEditIcon(category.icon); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/CategoryPanel.tsx` around lines 54 - 58, The parameter type for handleStartEdit is incorrect—replace the inline type { id: string; name: string; icon: string } with the shared Category type (or a union that matches both default and custom shapes) and use the correct property (label for defaults) when reading values; specifically update handleStartEdit to accept Category (or Category | CustomCategory) and setEditName to use category.name ?? category.label, keeping calls to setEditingId and setEditIcon unchanged so editing works for both default (label) and custom (name) categories.src/components/settings/AIConfigPanel.tsx (1)
107-107: Undocumented migration from 'minimal' to 'low' forreasoningEffort.The ternary silently maps
'minimal'to'low'. If this is intentional (e.g., deprecated value migration), consider adding a comment explaining why. If'minimal'is no longer valid, this migration should perhaps also happen on save to persist the corrected value.- reasoningEffort: (config.reasoningEffort === 'minimal' ? 'low' : config.reasoningEffort) || '', + // Migration: 'minimal' was renamed to 'low' in a previous version + reasoningEffort: (config.reasoningEffort === 'minimal' ? 'low' : config.reasoningEffort) || '',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/AIConfigPanel.tsx` at line 107, The code silently maps the deprecated 'minimal' value to 'low' when computing reasoningEffort in AIConfigPanel (reasoningEffort: (config.reasoningEffort === 'minimal' ? 'low' : config.reasoningEffort) || ''), so add a short inline comment explaining this migration and its reason, and also update the save/update path that persists settings (the form submit or save handler used by AIConfigPanel) to normalize config.reasoningEffort from 'minimal' to 'low' before writing so the corrected value is persisted.src/components/settings/BackupPanel.tsx (1)
105-106: Backup file selection relies on alphabetical sort of filenames.The code selects the latest backup via
backupFiles.sort().reverse()[0], which works because ISO date strings (github-stars-backup-YYYY-MM-DD.json) sort correctly alphabetically. However, this is fragile if:
- The filename format changes
- Multiple backups occur on the same day (no timestamp disambiguation)
Consider adding a timestamp to filenames or using file metadata for selection.
♻️ Add timestamp to backup filename for robustness
- const filename = `github-stars-backup-${new Date().toISOString().split('T')[0]}.json`; + const filename = `github-stars-backup-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;This produces filenames like
github-stars-backup-2026-04-14T18-30-00-000Z.jsonwhich:
- Still sort correctly alphabetically
- Support multiple backups per day
- Include full timestamp for clarity
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackupPanel.tsx` around lines 105 - 106, The current selection of the latest backup uses alphabetical sorting of backupFiles and picks latestBackup via backupFiles.sort().reverse()[0], which is fragile; change the backup creation and selection logic so filenames include ISO timestamps (e.g., use Date.toISOString with characters safe for filenames) and/or select latest by file metadata: when creating backups, generate names like github-stars-backup-YYYY-MM-DDTHH-mm-ss-SSSZ.json, and when picking the latest in BackupPanel.tsx replace the alphabetical heuristic with either (a) sorting backupFiles by parsed timestamp extracted from the filename (from the new timestamp suffix) or (b) fetching file metadata and choosing the file with the newest modified timestamp before calling webdavService.downloadFile(latestBackup). Ensure to update any code that produces the backup filename to the new timestamp format so the selector works reliably.src/components/SettingsPanel.tsx (1)
143-161: Mobile tab navigation may have horizontal scrolling issues.The mobile tab selector uses
overflow-x-autowhich allows horizontal scrolling, but there's no visual indicator (like a scroll shadow or fade) to hint that more tabs exist off-screen. With 6 tabs, users on small screens may not realize they can scroll.Consider adding scroll indicators or restructuring mobile navigation:
- Add gradient fade on edges when scrollable
- Use a dropdown/select for mobile instead of horizontal tabs
- Add left/right scroll arrows
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SettingsPanel.tsx` around lines 143 - 161, The mobile tab strip (the container with className "md:hidden ... overflow-x-auto" that renders tabs from the tabs array and uses setActiveTab/activeTab) needs a visual scroll indicator; add left/right gradient overlay elements positioned at the container edges and toggle their visibility based on the nav's scroll position (attach an onScroll handler to the <nav> element to compute scrollLeft, scrollWidth and clientWidth), or alternatively replace the horizontal strip with a native <select> for very small screens; ensure the gradient overlays are only shown when scrollLeft>0 (show left) or scrollLeft+clientWidth<scrollWidth (show right) so users know more tabs exist.src/components/settings/BackendPanel.tsx (2)
68-90: Partial sync failure leaves data in inconsistent state.If any sync operation in
handleSyncToBackendthrows (e.g.,syncReleasesfails aftersyncRepositoriessucceeds), the user gets an error alert but the backend is left in a partially synced state. Consider either:
- Adding per-operation error handling with rollback
- Documenting this behavior to users
- Implementing a transactional approach if the backend supports it
♻️ Add per-operation tracking for better error reporting
const handleSyncToBackend = async () => { if (!backend.isAvailable) { alert(t('后端不可用', 'Backend not available')); return; } setIsSyncingToBackend(true); + const results = { repos: false, releases: false, ai: false, webdav: false, settings: false }; try { await backend.syncRepositories(repositories); + results.repos = true; await backend.syncReleases(releases); + results.releases = true; await backend.syncAIConfigs(aiConfigs); + results.ai = true; await backend.syncWebDAVConfigs(webdavConfigs); + results.webdav = true; await backend.syncSettings({ hiddenDefaultCategoryIds }); + results.settings = true; alert(t( `已同步到后端:仓库 ${repositories.length},发布 ${releases.length},AI配置 ${aiConfigs.length},WebDAV配置 ${webdavConfigs.length}`, `Synced to backend: repos ${repositories.length}, releases ${releases.length}, AI configs ${aiConfigs.length}, WebDAV configs ${webdavConfigs.length}` )); } catch (error) { console.error('Sync to backend failed:', error); - alert(`${t('同步失败', 'Sync failed')}: ${(error as Error).message}`); + const syncedItems = Object.entries(results).filter(([, v]) => v).map(([k]) => k).join(', '); + alert(`${t('同步部分失败', 'Sync partially failed')}: ${(error as Error).message}\n${t('已同步', 'Synced')}: ${syncedItems || t('无', 'none')}`); } finally { setIsSyncingToBackend(false); } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackendPanel.tsx` around lines 68 - 90, handleSyncToBackend performs several sequential backend calls (backend.syncRepositories, syncReleases, syncAIConfigs, syncWebDAVConfigs, syncSettings) and currently stops on the first error leaving the backend partially updated; update handleSyncToBackend to wrap each sync call with per-operation try/catch that records which operations succeeded and either attempts compensating rollback calls for already-applied operations (using the backend's delete/undo APIs or a new backend.rollback/transaction API if available) or collects and surfaces a detailed error summary to the user, and ensure setIsSyncingToBackend(false) still runs in finally; reference these symbols: handleSyncToBackend, backend.syncRepositories, backend.syncReleases, backend.syncAIConfigs, backend.syncWebDAVConfigs, backend.syncSettings.
111-122: Empty data arrays are silently ignored during restore.The conditionals like
if (repoData.repositories.length > 0)mean that if the backend returns an empty array (legitimate case: user cleared all repos), the local data won't be updated. This may be intentional to prevent accidental data loss, but could also cause confusion when users expect their empty state to sync.Consider either:
- Adding a comment explaining this is intentional protection
- Or allowing empty arrays to clear local state (after the confirm dialog already warned about overwrite)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackendPanel.tsx` around lines 111 - 122, The current checks (e.g., if (repoData.repositories.length > 0) setRepositories(...)) silently ignore legitimate empty arrays and therefore won't clear local state during restore; remove those length checks and always call setRepositories(repoData.repositories ?? []) and likewise always call setReleases(releaseData.releases ?? []), setAIConfigs(aiConfigData ?? []), and setWebDAVConfigs(webdavConfigData ?? []), so empty backend arrays correctly clear local state (keep the existing overwrite/confirm dialog as the guard). If you intended to keep the protection instead, add a clear comment above these conditionals explaining that empty arrays are intentionally ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/settings/AIConfigPanel.tsx`:
- Around line 88-92: When saving an edited config, don't pass the entire config
object to updateAIConfig because the store expects a partial update and
config.isActive is currently being reset to false; instead, look up the existing
config by editingId (e.g., from the aiConfigs state or a getAIConfigById
helper), build a minimal updates object that excludes id and merges fields to
change while explicitly preserving existingConfig.isActive, and call
updateAIConfig(editingId, updates); for new entries continue to call
addAIConfig(config).
In `@src/components/settings/BackendPanel.tsx`:
- Around line 123-132: The reconciliation currently iterates over
hiddenDefaultCategoryIds (local) to show categories then over
settingsData.hiddenDefaultCategoryIds to hide — missing categories that are
visible locally but should be hidden per fetched data. Replace this by either:
(A) iterating the fetched list settingsData.hiddenDefaultCategoryIds and calling
hideDefaultCategory for each string, and then iterating the local
hiddenDefaultCategoryIds and calling showDefaultCategory for any id not present
in the fetched list; or (B) atomically replace the store value with
settingsData.hiddenDefaultCategoryIds using setHiddenDefaultCategoryIds if
available. Use the variables hiddenDefaultCategoryIds,
settingsData.hiddenDefaultCategoryIds and the functions
showDefaultCategory/hideDefaultCategory (or setHiddenDefaultCategoryIds) to
implement the fix.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 321-322: The backup description text in BackupPanel.tsx
incorrectly says secrets are "encrypted" — update the translation strings used
in the two list items (the t(...) calls that currently read 'AI 服务配置(密钥已加密)' /
'AI service configurations (keys encrypted)' and 'WebDAV 配置(密码已加密)' / 'WebDAV
configurations (passwords encrypted)') to indicate that secrets are
masked/obfuscated (e.g., "密钥已脱敏" / "keys masked" and "密码已脱敏" / "passwords
masked") so the UI correctly reflects that values are replaced with '***' rather
than actually encrypted.
- Around line 133-146: The code uses the stale local variable
hiddenDefaultCategoryIds (from earlier destructuring) when calling
showDefaultCategory, so visibility reconciliation can be wrong after restore;
instead, read the fresh/current hiddenDefaultCategoryIds from the store right
before you iterate (e.g. via store.getState() or the component selector) and
iterate that fresh array to call showDefaultCategory, then iterate
backupData.hiddenDefaultCategoryIds to call hideDefaultCategory; reference the
showDefaultCategory and hideDefaultCategory calls and replace the first loop's
source with the live state value rather than the previously destructured
hiddenDefaultCategoryIds.
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 60-79: handleSaveEdit currently finds the category by editingId
and does a deleteCustomCategory + addCustomCategory which causes UI flicker and
loss of cascading updates; instead call the store's updateCustomCategory with
the category id and the updated fields (name: editName.trim(), icon: editIcon)
from useAppStore to perform an atomic update (preserve list position and trigger
repository cascade), and then clear state (setEditingId(null), setEditName(''),
setEditIcon('')) as before while keeping the empty-name validation.
In `@src/components/settings/WebDAVPanel.tsx`:
- Around line 52-60: The current config object creation in WebDAVPanel (variable
config) always sets isActive: false which deactivates an existing config when
editing; change the logic in the save/submit handler that constructs the
WebDAVConfig so that when editingId is present you lookup the existing
WebDAVConfig (by editingId) from the component's configs/state/props and reuse
its isActive value, otherwise default to false for new configs; update the code
path that constructs config (the block referencing id: editingId ||
Date.now().toString(), name: form.name, url: ..., etc.) to set isActive =
(editingId ? existingConfig.isActive : false).
- Around line 45-69: handleSave currently displays raw messages from
WebDAVService.validateConfig (which are hardcoded Chinese) via
alert(errors.join('\n')), breaking localization; update handleSave to translate
those errors before showing them: call WebDAVService.validateConfig(form) as
before, then map each returned error string to a translation key (or fallback)
using the i18n t function (e.g., translate known Chinese messages like "WebDAV
URL是必需的" to a t('settings.webdav.validation.urlRequired') key), and finally call
alert(translatedErrors.join('\n')); keep references to handleSave,
WebDAVService.validateConfig, and the alert call so reviewers can locate and
verify the change.
---
Nitpick comments:
In `@src/components/settings/AIConfigPanel.tsx`:
- Line 107: The code silently maps the deprecated 'minimal' value to 'low' when
computing reasoningEffort in AIConfigPanel (reasoningEffort:
(config.reasoningEffort === 'minimal' ? 'low' : config.reasoningEffort) || ''),
so add a short inline comment explaining this migration and its reason, and also
update the save/update path that persists settings (the form submit or save
handler used by AIConfigPanel) to normalize config.reasoningEffort from
'minimal' to 'low' before writing so the corrected value is persisted.
In `@src/components/settings/BackendPanel.tsx`:
- Around line 68-90: handleSyncToBackend performs several sequential backend
calls (backend.syncRepositories, syncReleases, syncAIConfigs, syncWebDAVConfigs,
syncSettings) and currently stops on the first error leaving the backend
partially updated; update handleSyncToBackend to wrap each sync call with
per-operation try/catch that records which operations succeeded and either
attempts compensating rollback calls for already-applied operations (using the
backend's delete/undo APIs or a new backend.rollback/transaction API if
available) or collects and surfaces a detailed error summary to the user, and
ensure setIsSyncingToBackend(false) still runs in finally; reference these
symbols: handleSyncToBackend, backend.syncRepositories, backend.syncReleases,
backend.syncAIConfigs, backend.syncWebDAVConfigs, backend.syncSettings.
- Around line 111-122: The current checks (e.g., if
(repoData.repositories.length > 0) setRepositories(...)) silently ignore
legitimate empty arrays and therefore won't clear local state during restore;
remove those length checks and always call setRepositories(repoData.repositories
?? []) and likewise always call setReleases(releaseData.releases ?? []),
setAIConfigs(aiConfigData ?? []), and setWebDAVConfigs(webdavConfigData ?? []),
so empty backend arrays correctly clear local state (keep the existing
overwrite/confirm dialog as the guard). If you intended to keep the protection
instead, add a clear comment above these conditionals explaining that empty
arrays are intentionally ignored.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 105-106: The current selection of the latest backup uses
alphabetical sorting of backupFiles and picks latestBackup via
backupFiles.sort().reverse()[0], which is fragile; change the backup creation
and selection logic so filenames include ISO timestamps (e.g., use
Date.toISOString with characters safe for filenames) and/or select latest by
file metadata: when creating backups, generate names like
github-stars-backup-YYYY-MM-DDTHH-mm-ss-SSSZ.json, and when picking the latest
in BackupPanel.tsx replace the alphabetical heuristic with either (a) sorting
backupFiles by parsed timestamp extracted from the filename (from the new
timestamp suffix) or (b) fetching file metadata and choosing the file with the
newest modified timestamp before calling
webdavService.downloadFile(latestBackup). Ensure to update any code that
produces the backup filename to the new timestamp format so the selector works
reliably.
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 41-46: The newCategory object uses id: `custom-${Date.now()}`
which can collide; update the ID generation in the CategoryPanel component to
use a robust UUID (e.g., call crypto.randomUUID()) instead of Date.now() when
constructing newCategory (referencing the newCategory variable and
newCategoryName/newCategoryIcon fields), and add a small fallback (e.g.,
fallback to a random string) if crypto.randomUUID is undefined to preserve
compatibility.
- Around line 54-58: The parameter type for handleStartEdit is incorrect—replace
the inline type { id: string; name: string; icon: string } with the shared
Category type (or a union that matches both default and custom shapes) and use
the correct property (label for defaults) when reading values; specifically
update handleStartEdit to accept Category (or Category | CustomCategory) and
setEditName to use category.name ?? category.label, keeping calls to
setEditingId and setEditIcon unchanged so editing works for both default (label)
and custom (name) categories.
In `@src/components/settings/GeneralPanel.tsx`:
- Line 81: Replace the hardcoded "v0.3.0" in the GeneralPanel JSX translation
call by sourcing the app version from a single source of truth (e.g., import
VERSION from package.json or a central constants module) and pass it into the
translation (use a translation key with a placeholder or build the localized
string dynamically) so the displayed version is always the imported VERSION;
update the call site where t(...) is used in GeneralPanel to use that imported
VERSION value instead of the literal string.
In `@src/components/SettingsPanel.tsx`:
- Around line 143-161: The mobile tab strip (the container with className
"md:hidden ... overflow-x-auto" that renders tabs from the tabs array and uses
setActiveTab/activeTab) needs a visual scroll indicator; add left/right gradient
overlay elements positioned at the container edges and toggle their visibility
based on the nav's scroll position (attach an onScroll handler to the <nav>
element to compute scrollLeft, scrollWidth and clientWidth), or alternatively
replace the horizontal strip with a native <select> for very small screens;
ensure the gradient overlays are only shown when scrollLeft>0 (show left) or
scrollLeft+clientWidth<scrollWidth (show right) so users know more tabs exist.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 57bf0ba5-40c3-46cc-9189-078556f65dfc
⛔ Files ignored due to path filters (1)
dist/index.htmlis excluded by!**/dist/**
📒 Files selected for processing (8)
src/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/GeneralPanel.tsxsrc/components/settings/WebDAVPanel.tsxsrc/components/settings/index.ts
添加数据管理面板组件,支持查看数据统计和选择性删除不同类型的数据 包括仓库数据、发布信息、AI配置、WebDAV配置和分类设置 提供危险区域可删除所有数据,需GitHub用户名验证
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/components/settings/DataManagementPanel.tsx (2)
79-87: Toast timeouts may fire after component unmount.The
setTimeoutcalls inshowSuccessandshowErroraren't cleaned up on unmount, which could cause state updates on an unmounted component.Consider using
useEffectcleanup or storing timeout IDs in refs for cancellation.♻️ Optional: Add timeout cleanup
+import React, { useState, useCallback, useRef, useEffect } from 'react'; + +// Inside component: +const successTimeoutRef = useRef<NodeJS.Timeout>(); +const errorTimeoutRef = useRef<NodeJS.Timeout>(); + +useEffect(() => { + return () => { + if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current); + if (errorTimeoutRef.current) clearTimeout(errorTimeoutRef.current); + }; +}, []); const showSuccess = useCallback((message: string) => { setShowSuccessMessage(message); - setTimeout(() => setShowSuccessMessage(null), 3000); + successTimeoutRef.current = setTimeout(() => setShowSuccessMessage(null), 3000); }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/DataManagementPanel.tsx` around lines 79 - 87, The setTimeouts in showSuccess and showError can run after unmount causing state updates on an unmounted component; modify these functions to store their timeout IDs (e.g., in refs like successTimeoutRef and errorTimeoutRef) and clear any existing timeout before creating a new one, and add a useEffect cleanup that clears both refs on unmount; ensure you still call setShowSuccessMessage / setShowErrorMessage and clear the timeout refs when the timeout runs or when unmounting.
183-204: Category deletion uses potentially unsafe iteration pattern.The
forEachloop deletes categories one by one, but ifdeleteCustomCategorytriggers any async side effects, they won't be awaited. Additionally, iterating overstore.customCategorieswhile mutating it viadeleteCustomCategorycould cause unexpected behavior if the store's array reference changes mid-iteration.Consider using a safer approach:
♻️ Suggested safer deletion pattern
const deleteCategorySettings = async () => { try { const store = useAppStore.getState(); - // Reset category-related state - store.customCategories.forEach((cat) => { - store.deleteCustomCategory(cat.id); - }); + // Get category IDs first to avoid mutation during iteration + const categoryIds = store.customCategories.map((cat) => cat.id); + categoryIds.forEach((id) => store.deleteCustomCategory(id)); // Clear hidden default categories useAppStore.setState({ hiddenDefaultCategoryIds: [] }); - await clearAllStorage(); addLog(t('删除分类显示设置数据', 'Delete category display settings'), true);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/DataManagementPanel.tsx` around lines 183 - 204, The deleteCategorySettings function is mutating store.customCategories while iterating it with forEach which is unsafe if deleteCustomCategory has async side-effects or changes the array; fix by capturing a stable list of IDs first (const ids = useAppStore.getState().customCategories.map(c=>c.id)), then iterate that list using a sequential for...of that awaits deleteCustomCategory(id) (or Promise.all(ids.map(id=>deleteCustomCategory(id))) if parallel deletion is safe), and avoid referencing store.customCategories inside the deletion loop; keep useAppStore.getState(), deleteCustomCategory, clearAllStorage, addLog and showSuccess/showError usages intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 89-109: The clearAllStorage function is being invoked by selective
delete handlers (deleteRepositories, deleteReleases, deleteAIConfigs,
deleteWebDAVConfigs, deleteCategorySettings) and it unconditionally wipes the
github-stars-manager IndexedDB entry and other persisted state; remove calls to
clearAllStorage from those selective delete functions and instead only perform
the in-memory Zustand slice resets (i.e., call the specific slice reset/clear
methods you already have for repositories, releases, aiConfigs, webDAVConfigs,
categorySettings) so the persist middleware updates storage automatically; keep
clearAllStorage reserved only for deleteAllData which should continue to fully
clear localStorage, sessionStorage and IndexedDB.
- Around line 104-108: The catch in the indexedDBStorage.removeItem call inside
the clearAllStorage function silently swallows failures; change it to surface
the error (either rethrow the caught error or return a failure status) so
callers can detect partial failure. Specifically, update the catch around
indexedDBStorage.removeItem('github-stars-manager') to either throw the error
(throw error) or set/return a boolean/result object (e.g., { success: false,
reason: error }) and then adjust callers that show the success toast to check
this result before reporting success.
In `@src/components/SettingsPanel.tsx`:
- Around line 131-177: The layout bug comes from the top-level container div
whose class is "flex flex-1 overflow-hidden" causing the mobile nav (the div
with "md:hidden" that renders the mobile tab selector) and the content area (the
div containing renderTabContent()) to sit in a row; change that container to use
a column layout on small screens and switch to row at md (e.g., make it
responsive like "flex flex-col md:flex-row ...") so the mobile nav stacks above
the content while desktop keeps the sidebar/content row arrangement.
---
Nitpick comments:
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 79-87: The setTimeouts in showSuccess and showError can run after
unmount causing state updates on an unmounted component; modify these functions
to store their timeout IDs (e.g., in refs like successTimeoutRef and
errorTimeoutRef) and clear any existing timeout before creating a new one, and
add a useEffect cleanup that clears both refs on unmount; ensure you still call
setShowSuccessMessage / setShowErrorMessage and clear the timeout refs when the
timeout runs or when unmounting.
- Around line 183-204: The deleteCategorySettings function is mutating
store.customCategories while iterating it with forEach which is unsafe if
deleteCustomCategory has async side-effects or changes the array; fix by
capturing a stable list of IDs first (const ids =
useAppStore.getState().customCategories.map(c=>c.id)), then iterate that list
using a sequential for...of that awaits deleteCustomCategory(id) (or
Promise.all(ids.map(id=>deleteCustomCategory(id))) if parallel deletion is
safe), and avoid referencing store.customCategories inside the deletion loop;
keep useAppStore.getState(), deleteCustomCategory, clearAllStorage, addLog and
showSuccess/showError usages intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 708c2265-3b60-4586-8c41-ce051c81d26c
⛔ Files ignored due to path filters (1)
dist/index.htmlis excluded by!**/dist/**
📒 Files selected for processing (3)
src/components/SettingsPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/settings/index.ts
- 新增侧边栏折叠状态管理,支持快捷键(Ctrl+B)切换 - 为移动端和桌面端分别优化侧边栏显示方式 - 添加返回顶部按钮组件,提升长页面浏览体验 - 更新相关状态管理以支持新功能
|
Tip For best results, initiate chat on the files or code changes.
Feel free to share more details and I'll be happy to help! |
|
@coderabbitai review the PR |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/components/CategorySidebar.tsx (1)
273-292: Consider the UX impact of limiting collapsed categories to 8.The collapsed sidebar shows only the first 8 categories (
allCategories.slice(0, 8)). If a user has many custom categories, they won't be accessible in collapsed mode without expanding the sidebar first. This may be intentional, but worth noting that custom categories added after the 8th position won't have quick access in collapsed mode.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 273 - 292, The collapsed sidebar currently limits rendered icons with allCategories.slice(0, 8), which hides user-added categories beyond the 8th; change this by removing the hard-coded slice and either render allCategories (with a vertical scroll via CSS) or make the limit configurable (e.g., collapsedLimit prop/constant) and use that instead; update the map call around allCategories.slice(0, 8) in the CategorySidebar component (the block using selectedCategory, onCategorySelect, and category.icon) so collapsed mode shows all accessible categories or exposes a "more" affordance rather than silently truncating.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/BackToTop.tsx`:
- Around line 52-53: The aria-label and title in BackToTop.tsx are hardcoded in
Chinese; replace them with i18n lookups. Import and use the project's i18n
utility (e.g., useTranslation/t or translate function used elsewhere) inside the
BackToTop component and replace aria-label="回到顶部" and title="回到顶部" with the
translated key (e.g., t('backToTop') or translate('backToTop')); add the
corresponding i18n key/value entries for both Chinese and English in the locale
files. Ensure the call is available in the component scope so both aria-label
and title use the same translated string.
In `@src/components/CategorySidebar.tsx`:
- Around line 58-68: The global keyboard handler inside the useEffect
(handleKeyDown) toggles the sidebar on Ctrl/Cmd+B even when focus is inside
inputs, textareas, or contenteditable elements; update handleKeyDown to first
inspect document.activeElement and skip handling if the active element is an
input, textarea, or has isContentEditable === true (also consider checking
common editable roles like role="textbox"), then only call toggleSidebar() when
not focused in an editable element; keep the existing e.preventDefault() and key
checks but add this early-return guard within handleKeyDown to avoid interfering
with in-field shortcuts.
---
Nitpick comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 273-292: The collapsed sidebar currently limits rendered icons
with allCategories.slice(0, 8), which hides user-added categories beyond the
8th; change this by removing the hard-coded slice and either render
allCategories (with a vertical scroll via CSS) or make the limit configurable
(e.g., collapsedLimit prop/constant) and use that instead; update the map call
around allCategories.slice(0, 8) in the CategorySidebar component (the block
using selectedCategory, onCategorySelect, and category.icon) so collapsed mode
shows all accessible categories or exposes a "more" affordance rather than
silently truncating.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5742860d-eb4f-481f-a06d-0ac5053ac17b
⛔ Files ignored due to path filters (1)
dist/index.htmlis excluded by!**/dist/**
📒 Files selected for processing (5)
src/App.tsxsrc/components/BackToTop.tsxsrc/components/CategorySidebar.tsxsrc/store/useAppStore.tssrc/types/index.ts
✅ Files skipped from review due to trivial changes (1)
- src/types/index.ts
|
@coderabbitai generate docstrings |
✅ Actions performedInitiated docstring generation; will generate only if new commits exist. |
|
Caution Docstrings generation - FAILED An unexpected error occurred while opening a pull request: Cannot open a stacked PR on a forked repository. |
|
@coderabbitai review the PR |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/components/settings/WebDAVPanel.tsx (1)
61-78:⚠️ Potential issue | 🟡 MinorBug: Editing a config always sets
isActive: false, potentially deactivating an active config.When editing an existing WebDAV config, the saved config always has
isActive: false(line 68). If the user edits their currently active config, it will be deactivated upon save.This mirrors the fix already applied in
AIConfigPanel.tsx(lines 74-90).🐛 Proposed fix to preserve isActive state
const handleSave = () => { const errors = WebDAVService.validateConfig(form); if (errors.length > 0) { // ... translation logic } + const existingConfig = editingId ? webdavConfigs.find(c => c.id === editingId) : null; + const config: WebDAVConfig = { id: editingId || Date.now().toString(), name: form.name, url: form.url.replace(/\/$/, ''), username: form.username, password: form.password, path: form.path, - isActive: false, + isActive: existingConfig?.isActive ?? false, }; if (editingId) { updateWebDAVConfig(editingId, config); } else { addWebDAVConfig(config); } resetForm(); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/WebDAVPanel.tsx` around lines 61 - 78, When building the config in WebDAVPanel.tsx, you currently always set isActive: false which deactivates an existing config on edit; change the logic so that when editing (editingId is truthy) you preserve the existing config's isActive value instead of forcing false — retrieve the current config's isActive (or read it from state/props) and assign that to config.isActive before calling updateWebDAVConfig(editingId, config); keep addWebDAVConfig(config) creating new configs with isActive: false, and mirror the same preservation approach used in AIConfigPanel.tsx for reference.
🧹 Nitpick comments (5)
src/components/CategorySidebar.tsx (1)
282-298: Consider indicating when categories are truncated.The collapsed view displays only the first 8 categories via
allCategories.slice(0, 8). If users have more categories, there's no visual indication that additional categories exist. Consider adding a visual cue (e.g., a badge showing "+N more" or an overflow indicator) whenallCategories.length > 8.Additionally, the collapsed view loses the drag-and-drop functionality for quick category assignment that exists in the expanded view.
💡 Suggested improvement to show overflow indicator
{/* 折叠状态下的分类图标列表 */} <div className="flex flex-col items-center space-y-2"> {allCategories.slice(0, 8).map((category) => { const isSelected = selectedCategory === category.id; return ( <button key={category.id} onClick={() => onCategorySelect(category.id)} className={`w-8 h-8 flex items-center justify-center rounded-lg text-lg transition-all duration-200 ${ isSelected ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 ring-2 ring-blue-400' : 'hover:bg-gray-100 dark:hover:bg-gray-700' }`} title={category.name} > {category.icon} </button> ); })} + {allCategories.length > 8 && ( + <button + onClick={toggleSidebar} + className="w-8 h-8 flex items-center justify-center rounded-lg text-xs text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700" + title={t(`还有 ${allCategories.length - 8} 个分类`, `${allCategories.length - 8} more categories`)} + > + +{allCategories.length - 8} + </button> + )} </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 282 - 298, The collapsed category list currently slices allCategories to 8 items in CategorySidebar, hiding the rest without indication and dropping drag-and-drop; update the render logic around allCategories.slice(0, 8).map(...) to show an overflow indicator when allCategories.length > 8 (e.g., a button/badge like "+N more" using the remaining count) and ensure that the overflow indicator preserves the same keyboard/click behavior (calls onCategorySelect or opens the full list) and drag-and-drop affordance present in the expanded view; make the indicator use the same styling toggles as the category buttons (respecting selectedCategory) and reuse existing handlers/components so you don’t duplicate drag-and-drop logic.src/components/settings/CategoryPanel.tsx (1)
42-49: Minor:isCustom: trueis set redundantly.The store's
addCustomCategorymethod already addsisCustom: true(persrc/store/useAppStore.ts:413-415). Setting it here is harmless but redundant.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/CategoryPanel.tsx` around lines 42 - 49, The newCategory object in CategoryPanel.tsx redundantly sets isCustom: true before calling addCustomCategory; remove the isCustom property from the newCategory literal (keep id, name, icon) and let addCustomCategory (in useAppStore) set isCustom internally to avoid duplication and keep responsibility in one place.src/components/settings/AIConfigPanel.tsx (1)
121-121: Minor: Defensive migration from deprecated'minimal'to'low'.The ternary handles legacy
reasoningEffortvalues, but theAIReasoningEfforttype (from imports) likely doesn't include'minimal'. Consider adding a type annotation or comment explaining this is a migration for legacy data.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/AIConfigPanel.tsx` at line 121, The mapping for config.reasoningEffort converts legacy 'minimal' to 'low' but lacks explicit typing/documentation; update the assignment for reasoningEffort to either cast the result to the AIReasoningEffort type or add a brief inline comment explaining this is a defensive migration for legacy values, referencing the config object and AIReasoningEffort type, so maintainers know why 'minimal' is handled despite not being in the current type.src/components/settings/BackupPanel.tsx (1)
99-106: Note: Backup filename uses date only, so multiple backups per day overwrite.Line 66 generates filenames using only the date (
YYYY-MM-DD), so multiple backups on the same day will overwrite each other. The restore logic (lines 105-106) correctly selects the latest by sorting. This is acceptable behavior but worth noting for users who expect to keep multiple backups per day.💡 Optional: Include timestamp for multiple backups per day
-const filename = `github-stars-backup-${new Date().toISOString().split('T')[0]}.json`; +const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); +const filename = `github-stars-backup-${timestamp}.json`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackupPanel.tsx` around lines 99 - 106, Backup filenames currently use only the date so multiple backups in one day overwrite; update the backup filename generation (the code that builds the 'github-stars-backup-YYYY-MM-DD' name) to include a time component (e.g., HHMMSS or ISO timestamp) so each backup is unique per run, and keep the restore logic that filters via backupFiles and selects latestBackup (backupFiles.sort().reverse()[0]) unchanged so it continues to pick the newest file; ensure webdavService.downloadFile(latestBackup) still works with the new filename format.src/components/settings/BackendPanel.tsx (1)
123-136: Optimize category visibility reconciliation to match the pattern in autoSync.ts.The current implementation calls
hideDefaultCategory()for all server categories, relying on idempotency to avoid redundant operations. However,src/services/autoSync.ts(lines 153–164) already demonstrates a more efficient pattern: check the current state before calling hide/show functions, only triggering them when necessary.♻️ Refactor to match autoSync pattern
// 从服务端数据中隐藏所有应隐藏的分类 if (Array.isArray(settingsData.hiddenDefaultCategoryIds)) { - for (const categoryId of settingsData.hiddenDefaultCategoryIds) { - if (typeof categoryId === 'string') hideDefaultCategory(categoryId); - } - } - // 显示本地隐藏列表中但服务端没有隐藏的分类(即本地手动显示的) - if (Array.isArray(hiddenDefaultCategoryIds)) { - for (const categoryId of hiddenDefaultCategoryIds) { - if (typeof categoryId === 'string' && !settingsData.hiddenDefaultCategoryIds?.includes(categoryId)) { - showDefaultCategory(categoryId); - } + const nextHiddenIds = settingsData.hiddenDefaultCategoryIds.filter((id): id is string => typeof id === 'string'); + const currentHiddenIds = hiddenDefaultCategoryIds || []; + for (const id of currentHiddenIds) { + if (!nextHiddenIds.includes(id)) { + showDefaultCategory(id); + } + } + for (const id of nextHiddenIds) { + if (!currentHiddenIds.includes(id)) { + hideDefaultCategory(id); } } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackendPanel.tsx` around lines 123 - 136, The reconciliation loop currently blindly calls hideDefaultCategory/showDefaultCategory for IDs from settingsData.hiddenDefaultCategoryIds and hiddenDefaultCategoryIds; update it to follow the autoSync.ts pattern by checking current visibility before invoking actions so we only call hideDefaultCategory(categoryId) or showDefaultCategory(categoryId) when the category's existing visibility differs from the desired state. Use the same identifiers from this diff (settingsData.hiddenDefaultCategoryIds, hiddenDefaultCategoryIds) and the functions hideDefaultCategory / showDefaultCategory to implement the conditional checks and avoid redundant calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/components/settings/WebDAVPanel.tsx`:
- Around line 61-78: When building the config in WebDAVPanel.tsx, you currently
always set isActive: false which deactivates an existing config on edit; change
the logic so that when editing (editingId is truthy) you preserve the existing
config's isActive value instead of forcing false — retrieve the current config's
isActive (or read it from state/props) and assign that to config.isActive before
calling updateWebDAVConfig(editingId, config); keep addWebDAVConfig(config)
creating new configs with isActive: false, and mirror the same preservation
approach used in AIConfigPanel.tsx for reference.
---
Nitpick comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 282-298: The collapsed category list currently slices
allCategories to 8 items in CategorySidebar, hiding the rest without indication
and dropping drag-and-drop; update the render logic around
allCategories.slice(0, 8).map(...) to show an overflow indicator when
allCategories.length > 8 (e.g., a button/badge like "+N more" using the
remaining count) and ensure that the overflow indicator preserves the same
keyboard/click behavior (calls onCategorySelect or opens the full list) and
drag-and-drop affordance present in the expanded view; make the indicator use
the same styling toggles as the category buttons (respecting selectedCategory)
and reuse existing handlers/components so you don’t duplicate drag-and-drop
logic.
In `@src/components/settings/AIConfigPanel.tsx`:
- Line 121: The mapping for config.reasoningEffort converts legacy 'minimal' to
'low' but lacks explicit typing/documentation; update the assignment for
reasoningEffort to either cast the result to the AIReasoningEffort type or add a
brief inline comment explaining this is a defensive migration for legacy values,
referencing the config object and AIReasoningEffort type, so maintainers know
why 'minimal' is handled despite not being in the current type.
In `@src/components/settings/BackendPanel.tsx`:
- Around line 123-136: The reconciliation loop currently blindly calls
hideDefaultCategory/showDefaultCategory for IDs from
settingsData.hiddenDefaultCategoryIds and hiddenDefaultCategoryIds; update it to
follow the autoSync.ts pattern by checking current visibility before invoking
actions so we only call hideDefaultCategory(categoryId) or
showDefaultCategory(categoryId) when the category's existing visibility differs
from the desired state. Use the same identifiers from this diff
(settingsData.hiddenDefaultCategoryIds, hiddenDefaultCategoryIds) and the
functions hideDefaultCategory / showDefaultCategory to implement the conditional
checks and avoid redundant calls.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 99-106: Backup filenames currently use only the date so multiple
backups in one day overwrite; update the backup filename generation (the code
that builds the 'github-stars-backup-YYYY-MM-DD' name) to include a time
component (e.g., HHMMSS or ISO timestamp) so each backup is unique per run, and
keep the restore logic that filters via backupFiles and selects latestBackup
(backupFiles.sort().reverse()[0]) unchanged so it continues to pick the newest
file; ensure webdavService.downloadFile(latestBackup) still works with the new
filename format.
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 42-49: The newCategory object in CategoryPanel.tsx redundantly
sets isCustom: true before calling addCustomCategory; remove the isCustom
property from the newCategory literal (keep id, name, icon) and let
addCustomCategory (in useAppStore) set isCustom internally to avoid duplication
and keep responsibility in one place.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5294dc69-4871-43c3-93ea-440ebc24e21d
📒 Files selected for processing (9)
src/components/BackToTop.tsxsrc/components/CategorySidebar.tsxsrc/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/WebDAVPanel.tsx
✅ Files skipped from review due to trivial changes (1)
- src/components/settings/DataManagementPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/BackToTop.tsx
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
src/components/settings/WebDAVPanel.tsx (1)
61-75:⚠️ Potential issue | 🟠 MajorPreserve
isActivewhen editing an existing WebDAV config.Line 68 still hardcodes
isActive: false, so saving an edited active config will deactivate it.🐛 Suggested fix
- const config: WebDAVConfig = { - id: editingId || Date.now().toString(), - name: form.name, - url: form.url.replace(/\/$/, ''), - username: form.username, - password: form.password, - path: form.path, - isActive: false, - }; - if (editingId) { - updateWebDAVConfig(editingId, config); + const existingConfig = webdavConfigs.find(c => c.id === editingId); + if (!existingConfig) return; + + updateWebDAVConfig(editingId, { + name: form.name, + url: form.url.replace(/\/$/, ''), + username: form.username, + password: form.password, + path: form.path, + isActive: existingConfig.isActive, + }); } else { - addWebDAVConfig(config); + addWebDAVConfig({ + id: Date.now().toString(), + name: form.name, + url: form.url.replace(/\/$/, ''), + username: form.username, + password: form.password, + path: form.path, + isActive: false, + }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/WebDAVPanel.tsx` around lines 61 - 75, The code always sets isActive: false when building the WebDAVConfig, so edits will deactivate an active entry; change the construction to preserve the original isActive when editing by reading the existing config's isActive (e.g., fetch the current WebDAVConfig by editingId) and assign isActive: existing.isActive when editing, otherwise default to false for new configs; update the logic around updateWebDAVConfig and addWebDAVConfig to use this adjusted config so editingId paths keep the prior isActive value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/BackToTop.tsx`:
- Around line 24-29: The BackToTop component's useEffect currently only
registers the scroll listener, so initial visibility can be stale; inside the
same effect (where window.addEventListener and removeEventListener are used)
call toggleVisibility() once after adding the listener to initialize the button
state on mount, ensuring you still clean up with
window.removeEventListener(toggleVisibility) in the returned function.
- Around line 47-56: The BackToTop component currently only toggles
pointer-events via the isVisible CSS classes, which doesn't prevent keyboard
focus; update the interactive element (the BackToTop button rendered in
BackToTop) to be removed from the accessibility tree when hidden by: setting
aria-hidden={ !isVisible } and setting tabIndex to -1 when isVisible is false
(and restore tabIndex to 0 or undefined when true). Locate the root interactive
element in BackToTop (the element using isVisible classes and aria-label/title)
and add those accessibility guards so it cannot be focused or reached by
keyboard when visually hidden.
In `@src/components/CategorySidebar.tsx`:
- Around line 190-196: The icon-only sidebar buttons (e.g., the add-category
button wired to handleAddCategory and the collapsed category buttons in the same
component) rely only on title attributes which is not a robust accessible name;
update those buttons in CategorySidebar (including the button rendering the Plus
icon and the collapsed category buttons) to provide explicit accessible names by
adding aria-label attributes (use the same i18n key via t('添加分类','Add Category')
or equivalent) or include visually-hidden text inside the button so screen
readers get a proper name; ensure the aria-label text matches the
visible/tooltip text and keep the Plus icon as decorative (no role change
needed) so the screen reader announces the provided label.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 155-211: The restore currently merges configs; change it to fully
replace by first building the set of backup IDs (from backupData.aiConfigs and
backupData.webdavConfigs), then remove any local entries whose id is NOT in that
set (use removeAIConfig/removeWebDAVConfig), and only after pruning proceed with
the existing loop logic that calls updateAIConfig/addAIConfig and
updateWebDAVConfig/addWebDAVConfig; when applying entries preserve secrets if
the backup value is the mask ('***') by using the existing config's
apiKey/password as you already do. Ensure you compute currentMap and backupId
sets from aiConfigs/webdavConfigs and backupData.* before modifying collections
to avoid iteration issues.
- Around line 67-72: The current uploadFile(success) branch only handles the
true case and leaves users without feedback on failure; after calling
webdavService.uploadFile(filename, JSON.stringify(backupData, null, 2)) check
for a falsy return and in that branch call an error notification (e.g.,
alert(t('数据备份失败!','Data backup failed!')) or your app's toast), do not call
setLastBackup on failure, and also log the error (console.error or process
logger) so the failure path for uploadFile, filename, and backupData is explicit
and the UI spinner/disabled state is handled consistently.
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 89-109: clearAllStorage currently wipes too broadly (uses
sessionStorage.clear() and substring matches like "zustand"), risking deletion
of other apps' data; update clearAllStorage to only delete explicit app-owned
keys by maintaining a whitelist/prefix list (e.g., APP_KEY_PREFIX and explicit
key names) and iterate over localStorage and sessionStorage removing keys that
exactly match or startWith those prefixes, and remove only the specific
IndexedDB database/store name used by this app (retain the
indexedDBStorage.removeItem('github-stars-manager') call but do not call
sessionStorage.clear() or generic "zustand" substring matching); adjust the
logic in the clearAllStorage function to reference those explicit key
names/prefixes when deleting.
---
Duplicate comments:
In `@src/components/settings/WebDAVPanel.tsx`:
- Around line 61-75: The code always sets isActive: false when building the
WebDAVConfig, so edits will deactivate an active entry; change the construction
to preserve the original isActive when editing by reading the existing config's
isActive (e.g., fetch the current WebDAVConfig by editingId) and assign
isActive: existing.isActive when editing, otherwise default to false for new
configs; update the logic around updateWebDAVConfig and addWebDAVConfig to use
this adjusted config so editingId paths keep the prior isActive value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9151590d-c25a-42fc-b1fd-5a3ef6308a12
📒 Files selected for processing (9)
src/components/BackToTop.tsxsrc/components/CategorySidebar.tsxsrc/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/WebDAVPanel.tsx
✅ Files skipped from review due to trivial changes (1)
- src/components/settings/BackendPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/settings/CategoryPanel.tsx
为多个按钮组件添加aria-label属性,确保屏幕阅读器能正确识别按钮功能。同时修复BackToTop组件初始可见状态和DataManagementPanel中存储清理的逻辑
|
@coderabbitai review the PR |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
src/components/CategorySidebar.tsx (1)
395-425:⚠️ Potential issue | 🟡 MinorAdd explicit labels to the icon-only row actions.
The edit/delete/hide buttons still rely on
titleonly. Please add matchingaria-labels so screen readers can announce them reliably.♿ Minimal fix
<button onClick={(e) => { e.stopPropagation(); handleEditCategory(category); }} className="p-1 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600" title={t('编辑分类', 'Edit category')} + aria-label={t('编辑分类', 'Edit category')} > <Edit3 className="w-3.5 h-3.5" /> </button> {category.isCustom ? ( <button onClick={(e) => { e.stopPropagation(); void handleDeleteCategory(category); }} className="p-1 rounded-md text-red-500 hover:bg-red-100 dark:hover:bg-red-900/40" title={t('删除分类', 'Delete category')} + aria-label={t('删除分类', 'Delete category')} > <Trash2 className="w-3.5 h-3.5" /> </button> ) : ( <button onClick={(e) => { e.stopPropagation(); void handleHideDefaultCategory(category); }} className="p-1 rounded-md text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-600" title={t('隐藏默认分类', 'Hide default category')} + aria-label={t('隐藏默认分类', 'Hide default category')} > <EyeOff className="w-3.5 h-3.5" /> </button> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 395 - 425, The three icon-only buttons in CategorySidebar.tsx (the Edit, Delete, and Hide buttons that call handleEditCategory, handleDeleteCategory, and handleHideDefaultCategory) rely only on title attributes; add matching aria-label attributes to each button (e.g., aria-label={t('编辑分类','Edit category')} for the Edit button, aria-label={t('删除分类','Delete category')} for Delete, and aria-label={t('隐藏默认分类','Hide default category')} for the Hide button) so screen readers can announce them reliably, leaving existing title and click handlers intact.
🧹 Nitpick comments (1)
src/components/settings/BackupPanel.tsx (1)
124-130: Consider using fresh state forcustomCategoriesfor consistency.The code correctly uses
useAppStore.getState().hiddenDefaultCategoryIds(line 139) to get fresh state, but uses the potentially stalecustomCategoriesfrom the destructured render-time state here. While this works in practice (since the array reference is stable during iteration), using fresh state would be more consistent.♻️ Optional consistency fix
try { - if (Array.isArray(customCategories)) { - for (const cat of customCategories) { + const currentCustomCategories = useAppStore.getState().customCategories; + if (Array.isArray(currentCustomCategories)) { + for (const cat of currentCustomCategories) { if (cat && cat.id) { deleteCustomCategory(cat.id); } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackupPanel.tsx` around lines 124 - 130, Use fresh runtime state instead of the render-time destructured customCategories: call useAppStore.getState().customCategories to obtain the up-to-date array and iterate that when calling deleteCustomCategory(cat.id). Update the loop that currently references the destructured customCategories so it reads from useAppStore.getState().customCategories, still checking for cat && cat.id and calling deleteCustomCategory(cat.id).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 41-51: The desktop-first flash occurs because isMobile is
initialized to false; in CategorySidebar initialize isMobile from the actual
breakpoint or defer rendering until breakpoint is known: set the initial state
using a function that checks typeof window !== 'undefined' and returns
window.innerWidth < 1024 (or use null/undefined and render nothing until
checked), and keep the existing useEffect (or switch to useLayoutEffect) to
update on resize; update any conditional rendering in CategorySidebar to account
for the deferred/initial value so the desktop branch is not rendered on narrow
viewports before the effect runs.
- Line 272: Update the tooltip text to reflect the real shortcut (Ctrl OR Cmd)
in CategorySidebar: replace the hardcoded "Ctrl+B" string passed to the title
prop (the t(...) calls currently showing '展开侧栏 (Ctrl+B)' / 'Expand Sidebar
(Ctrl+B)') with either a platform-aware label (detect macOS and display 'Cmd+B',
otherwise 'Ctrl+B') or a combined label like 'Ctrl/Cmd+B'; make the same change
for the other title instance around the second occurrence so both tooltips match
the handler that accepts Ctrl or Cmd.
- Around line 58-75: The global keyboard handler in useEffect (handleKeyDown)
should ignore the shortcut when isMobile is true to avoid mutating persisted
isSidebarCollapsed while mobile layout always shows expanded; inside
handleKeyDown (or at top of the effect) return early when isMobile, and add
isMobile to the useEffect dependency array so the listener is added/removed
correctly when mobile state changes; update references to toggleSidebar and
handleKeyDown accordingly so the listener respects isMobile.
- Around line 352-428: The category row currently renders as a single <button>
(onCategorySelect) containing other interactive buttons (handleEditCategory,
handleDeleteCategory, handleHideDefaultCategory), which creates invalid nested
interactive elements; move the action toolbar out of the outer button into a
sibling absolutely positioned wrapper so the edit/delete/hide buttons are not
children of the row button. Concretely, wrap the row into a container (e.g., a
div with the current group classes), keep the primary selection element as a
single button (the one using onCategorySelect/onDragOver/onDrop), and render the
toolbar as a separate div sibling (the current absolute right-3 top-1/2 wrapper)
that uses the container's hover/focus-within state to reveal itself; preserve
existing event handlers (e.stopPropagation() calls) on
handleEditCategory/handleDeleteCategory/handleHideDefaultCategory and maintain
pointer-events toggling so toolbar buttons remain clickable. Also ensure the
toolbar becomes visible on keyboard focus by using focus-within on the shared
container (so keyboard users can access the actions).
In `@src/components/settings/BackupPanel.tsx`:
- Around line 111-113: The code currently assigns const backupContent = await
webdavService.downloadFile(latestBackup) and then proceeds only if truthy, but
if downloadFile returns null the restore silently does nothing; update the
restore flow in BackupPanel (the function handling the restore) to explicitly
handle a null return from webdavService.downloadFile(latestBackup): detect if
backupContent === null, show a user-facing error/notification (e.g., toast or
alert) indicating the backup download failed or backup not found, and abort the
restore early; alternatively you can remove the conditional and let JSON.parse
fail into the existing catch, but prefer explicit null handling for clearer user
feedback.
---
Duplicate comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 395-425: The three icon-only buttons in CategorySidebar.tsx (the
Edit, Delete, and Hide buttons that call handleEditCategory,
handleDeleteCategory, and handleHideDefaultCategory) rely only on title
attributes; add matching aria-label attributes to each button (e.g.,
aria-label={t('编辑分类','Edit category')} for the Edit button,
aria-label={t('删除分类','Delete category')} for Delete, and
aria-label={t('隐藏默认分类','Hide default category')} for the Hide button) so screen
readers can announce them reliably, leaving existing title and click handlers
intact.
---
Nitpick comments:
In `@src/components/settings/BackupPanel.tsx`:
- Around line 124-130: Use fresh runtime state instead of the render-time
destructured customCategories: call useAppStore.getState().customCategories to
obtain the up-to-date array and iterate that when calling
deleteCustomCategory(cat.id). Update the loop that currently references the
destructured customCategories so it reads from
useAppStore.getState().customCategories, still checking for cat && cat.id and
calling deleteCustomCategory(cat.id).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bcdb68cc-4177-4895-8214-67b5739b2b6d
📒 Files selected for processing (4)
src/components/BackToTop.tsxsrc/components/CategorySidebar.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/DataManagementPanel.tsx
✅ Files skipped from review due to trivial changes (1)
- src/components/settings/DataManagementPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/BackToTop.tsx
添加移动端标签导航组件,支持平滑滚动和触摸滑动切换 引入滚动条隐藏和触摸优化样式类 重构设置面板的标签切换逻辑,添加过渡动画效果
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (2)
src/components/SettingsPanel.tsx (2)
153-155:⚠️ Potential issue | 🟠 MajorKeep tab IDs unique across mobile and desktop navs.
Both tab navigations stay mounted and reuse the same
tab-${tab.id}IDs, so the panel’saria-labelledbycan resolve against duplicate elements. That makes the tab/panel association unreliable for assistive tech. Prefix the tab IDs by context or render only one tablist at a time.Also applies to: 315-316, 364-366, 421-423
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SettingsPanel.tsx` around lines 153 - 155, In SettingsPanel, tab IDs like `tab-${tab.id}` are duplicated between mobile and desktop tablists; prefix the IDs with the nav context (e.g., `tab-${navType}-${tab.id}` where `navType` is "mobile" or "desktop") or otherwise ensure only one tablist is rendered. Update every related attribute (`id`, `aria-controls` on the tab, and the panel's `id`/`aria-labelledby`) so they use the same prefixed value; adjust usages around `activeTab`, `aria-selected`, and the tabpanel lookup to match the new prefixed ID scheme.
346-349:⚠️ Potential issue | 🟡 MinorLocalize the close button’s accessible name.
This hard-codes English in an otherwise localized component, so screen readers will announce the wrong language in Chinese mode. Use
t(...)here too.Suggested fix
- aria-label="Close settings" + aria-label={t('关闭设置', 'Close settings')}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SettingsPanel.tsx` around lines 346 - 349, The Close button in SettingsPanel hardcodes an English aria-label; replace aria-label="Close settings" with a localized string using the existing translation function (e.g. aria-label={t('closeSettings')} or aria-label={t('settings.close')}) so screen readers get the correct language; ensure you use the same translation key convention used elsewhere in SettingsPanel and that the translation key exists in your i18n files, and keep the onClick handler handleClose unchanged.
🧹 Nitpick comments (4)
src/components/AssetFilterManager.tsx (2)
27-31:PRESET_ICON_MAP[pf.id]may returnundefinedfor unknown preset IDs.If
PRESET_FILTERScontains an entry with anidnot present inPRESET_ICON_MAP, the resulting filter will haveicon: undefined. While the component handles this gracefully with the fallbackconst Icon = preset.icon ? ICON_MAP[preset.icon] : Filter, consider adding a type guard or exhaustiveness check to catch mismatches during development.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/AssetFilterManager.tsx` around lines 27 - 31, DEFAULT_PRESET_FILTERS is assigning icon: PRESET_ICON_MAP[pf.id] which can be undefined for unknown ids; update the mapping to provide a safe fallback and/or assert validity: when building DEFAULT_PRESET_FILTERS from PRESET_FILTERS, check if PRESET_ICON_MAP[pf.id] exists and if not either assign a known default icon key (so icon is never undefined for UI lookup) or throw/log in dev to catch mismatches; reference PRESET_FILTERS, PRESET_ICON_MAP, DEFAULT_PRESET_FILTERS, and the runtime lookup that later uses ICON_MAP/preset.icon (and Filter fallback) to ensure every preset has a valid icon key.
85-99:handleResetPresetsmay create duplicate filters if called multiple times.The function deletes existing presets then adds defaults, but if
addAssetFilterdoesn't deduplicate by ID, calling reset while some presets are missing could lead to duplicates. Also, the loop callsonFilterTogglefor each selected preset being removed, which may cause unnecessary re-renders.Suggested improvement
const handleResetPresets = () => { if (confirm(language === 'zh' ? '确定要重置所有预设筛选器吗?这将恢复默认设置。' : 'Are you sure you want to reset all preset filters? This will restore default settings.')) { - // 删除所有现有的预设筛选器 - presetFilters.forEach(filter => { - deleteAssetFilter(filter.id); - if (selectedFilters.includes(filter.id)) { - onFilterToggle(filter.id); - } - }); - // 添加默认预设筛选器 - DEFAULT_PRESET_FILTERS.forEach(filter => { - addAssetFilter(filter); - }); + // Collect IDs to deselect first + const idsToDeselect = presetFilters + .filter(f => selectedFilters.includes(f.id)) + .map(f => f.id); + + // Delete existing presets + presetFilters.forEach(filter => deleteAssetFilter(filter.id)); + + // Add defaults + DEFAULT_PRESET_FILTERS.forEach(filter => addAssetFilter(filter)); + + // Batch deselect if needed (or clear filters once) + idsToDeselect.forEach(id => onFilterToggle(id)); } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/AssetFilterManager.tsx` around lines 85 - 99, handleResetPresets can produce duplicate filters and cause extra toggles; ensure you fully remove existing preset IDs and avoid per-item toggle calls before re-adding defaults. Specifically, in handleResetPresets use presetFilters and DEFAULT_PRESET_FILTERS to (1) compute the set of preset IDs to remove and call deleteAssetFilter for each, (2) determine which of those removed IDs are in selectedFilters and call onFilterToggle once per removed ID (or call it after the deletion pass in a single loop) to avoid repeated re-renders, and (3) when adding defaults via addAssetFilter, skip adding any default whose id already exists (or deduplicate by id) so repeated resets cannot create duplicates.src/components/RepositoryCard.tsx (1)
396-407: Consider addingaria-describedbyfor richer context.The
aria-labelprovides the repository name and description, which is good. For cards with AI analysis status or subscription state, consider addingaria-describedbypointing to the status indicators so screen reader users get the full context.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryCard.tsx` around lines 396 - 407, The container div in RepositoryCard should include an aria-describedby that references the status indicator elements so screen readers get AI analysis/subscription state; add id attributes to the status elements (e.g., ai-status and subscription-status or similar unique ids created inside the RepositoryCard) and compute a space-separated ariaDescribedBy string including those IDs when the indicators are rendered, then set aria-describedby={ariaDescribedBy} on the top-level element (the same element using aria-label and props like isSelected, onSelect, handleCardClick, handleCardKeyDown, repository) so the status text is announced.src/components/ReleaseTimeline.tsx (1)
808-810:max-h-[2000px]is a magic number that may clip long content.For releases with extensive notes or many assets, 2000px may be insufficient. Consider using a CSS variable or a more dynamic approach, or document why this value was chosen.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 808 - 810, The hardcoded max-h-[2000px] can clip long release notes; update the ReleaseTimeline component to avoid the magic number by measuring content height dynamically or using a CSS variable: add a ref to the expanding container, compute its scrollHeight (e.g., contentRef.current.scrollHeight) and replace the class max-h-[2000px] with an inline style like style={{ maxHeight: isExpanded ? `${contentHeight}px` : 0 }} (or replace max-h-[2000px] with max-h-[var(--release-max-height)] and define that variable globally), and ensure the isExpanded toggle uses this measured height so long content is never clipped; reference the isExpanded variable and the expanding container element in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/ReleaseTimeline.tsx`:
- Around line 147-164: The filter-matching logic is duplicated between
matchesActiveFilters and the inline filter inside the useMemo; refactor by
removing the inline duplicated logic and call matchesActiveFilters(link.name)
(or appropriate link property) from the useMemo filter callback instead,
ensuring matchesActiveFilters has access to selectedFilters, assetFilters and
PRESET_FILTERS (move it above the useMemo if needed) and remove the redundant
keyword matching block in the useMemo so all matching is centralized in
matchesActiveFilters.
- Around line 714-717: The clickable header div that calls
toggleReleaseExpansion(release.id) must expose the expansion state to assistive
tech; add an aria-expanded attribute bound to the expansion boolean (e.g.,
aria-expanded={isExpanded} or aria-expanded={isReleaseExpanded(release.id)}) on
the container with className "p-4 sm:p-6 cursor-pointer select-none" so screen
readers know whether the card is open or closed; ensure the same boolean
reflects the state used by toggleReleaseExpansion.
- Around line 54-65: The inline-vs-block detection in the code renderer (the
anonymous function passed as code: ({ children, className }) => { ... } inside
ReleaseTimeline) incorrectly assumes absence of className means inline; change
the logic to treat a node as block code when className is present OR the
children contain a newline (i.e., fenced/multi-line code), and only treat as
inline when there is no className and the children are a single-line string;
update the isInline calculation accordingly and keep the two JSX branches
(inline <code> vs block <pre><code>) the same.
In `@src/components/RepositoryList.tsx`:
- Around line 565-601: The try/catch around updateRepository in
handleBulkCategorize is ineffective because updateRepository (from zustand) is
synchronous and won’t throw; either (A) remove the per-repo try/catch and treat
updateRepository as synchronous, then wrap await forceSyncToBackend() in a
try/catch to capture backend sync errors and report them (use failedRepos to
record any sync-level failure), or (B) if you intend per-repo async failures,
make updateRepository return a Promise, await each call (or Promise.all with
mapping over filteredRepositories & selectedRepoIds), catch rejections and push
repo.full_name into failedRepos; update successCount calculation accordingly and
keep references to handleBulkCategorize, updateRepository, forceSyncToBackend,
filteredRepositories, and selectedRepoIds when implementing the chosen fix.
- Around line 463-524: The bulk "ai-summary" case lacks progress reporting and
stop/pause support; refactor it to reuse the existing analysis flow (e.g., call
or mimic handleAIAnalyze) or integrate the same controls: use
setAnalysisProgress to report current index/total and successCount, check
shouldStopRef before processing each repo to abort early, and respect isPaused
(await until unpaused) inside the batch loop; keep the core calls to
GitHubApiService, AIService, resolveCategoryAssignment, updateRepository, and
forceSyncToBackend but add progress updates, stop handling, and pause handling
so the UI can show progress and allow cancellation/pausing.
In `@src/components/SettingsPanel.tsx`:
- Around line 53-70: updateIndicator can become stale when the nav layout
changes (e.g., hidden via md:hidden/lg:hidden or viewport resize); attach a
ResizeObserver to scrollContainerRef (and a window 'resize' fallback) to call
updateIndicator whenever the container size changes, ensuring you reuse the
existing rafRef and tabRefs logic and call setIndicatorStyle from
updateIndicator; create the observer after scrollContainerRef.current is
available, start by calling updateIndicator once, and cleanly disconnect the
ResizeObserver and remove the window listener in the component cleanup to avoid
leaks.
- Around line 332-397: The modal currently lacks keyboard focus handling; update
SettingsPanel to save document.activeElement before opening, move focus into the
modal (e.g., focus the first focusable in the modal container ref), trap
Tab/Shift+Tab within the modal, close on Escape by calling handleClose, and
restore focus to the saved element on close; implement these behaviors in a
useEffect tied to the modal open state, attach keydown handlers and focus-wrap
logic to the modal container ref (the same element that renders the dialog), and
ensure background content is made inert/aria-hidden while the modal is open so
users cannot tab into the page behind it.
---
Duplicate comments:
In `@src/components/SettingsPanel.tsx`:
- Around line 153-155: In SettingsPanel, tab IDs like `tab-${tab.id}` are
duplicated between mobile and desktop tablists; prefix the IDs with the nav
context (e.g., `tab-${navType}-${tab.id}` where `navType` is "mobile" or
"desktop") or otherwise ensure only one tablist is rendered. Update every
related attribute (`id`, `aria-controls` on the tab, and the panel's
`id`/`aria-labelledby`) so they use the same prefixed value; adjust usages
around `activeTab`, `aria-selected`, and the tabpanel lookup to match the new
prefixed ID scheme.
- Around line 346-349: The Close button in SettingsPanel hardcodes an English
aria-label; replace aria-label="Close settings" with a localized string using
the existing translation function (e.g. aria-label={t('closeSettings')} or
aria-label={t('settings.close')}) so screen readers get the correct language;
ensure you use the same translation key convention used elsewhere in
SettingsPanel and that the translation key exists in your i18n files, and keep
the onClick handler handleClose unchanged.
---
Nitpick comments:
In `@src/components/AssetFilterManager.tsx`:
- Around line 27-31: DEFAULT_PRESET_FILTERS is assigning icon:
PRESET_ICON_MAP[pf.id] which can be undefined for unknown ids; update the
mapping to provide a safe fallback and/or assert validity: when building
DEFAULT_PRESET_FILTERS from PRESET_FILTERS, check if PRESET_ICON_MAP[pf.id]
exists and if not either assign a known default icon key (so icon is never
undefined for UI lookup) or throw/log in dev to catch mismatches; reference
PRESET_FILTERS, PRESET_ICON_MAP, DEFAULT_PRESET_FILTERS, and the runtime lookup
that later uses ICON_MAP/preset.icon (and Filter fallback) to ensure every
preset has a valid icon key.
- Around line 85-99: handleResetPresets can produce duplicate filters and cause
extra toggles; ensure you fully remove existing preset IDs and avoid per-item
toggle calls before re-adding defaults. Specifically, in handleResetPresets use
presetFilters and DEFAULT_PRESET_FILTERS to (1) compute the set of preset IDs to
remove and call deleteAssetFilter for each, (2) determine which of those removed
IDs are in selectedFilters and call onFilterToggle once per removed ID (or call
it after the deletion pass in a single loop) to avoid repeated re-renders, and
(3) when adding defaults via addAssetFilter, skip adding any default whose id
already exists (or deduplicate by id) so repeated resets cannot create
duplicates.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 808-810: The hardcoded max-h-[2000px] can clip long release notes;
update the ReleaseTimeline component to avoid the magic number by measuring
content height dynamically or using a CSS variable: add a ref to the expanding
container, compute its scrollHeight (e.g., contentRef.current.scrollHeight) and
replace the class max-h-[2000px] with an inline style like style={{ maxHeight:
isExpanded ? `${contentHeight}px` : 0 }} (or replace max-h-[2000px] with
max-h-[var(--release-max-height)] and define that variable globally), and ensure
the isExpanded toggle uses this measured height so long content is never
clipped; reference the isExpanded variable and the expanding container element
in your changes.
In `@src/components/RepositoryCard.tsx`:
- Around line 396-407: The container div in RepositoryCard should include an
aria-describedby that references the status indicator elements so screen readers
get AI analysis/subscription state; add id attributes to the status elements
(e.g., ai-status and subscription-status or similar unique ids created inside
the RepositoryCard) and compute a space-separated ariaDescribedBy string
including those IDs when the indicators are rendered, then set
aria-describedby={ariaDescribedBy} on the top-level element (the same element
using aria-label and props like isSelected, onSelect, handleCardClick,
handleCardKeyDown, repository) so the status text is announced.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f34298b7-564e-4af5-8a32-4458e50fa9a1
⛔ Files ignored due to path filters (1)
dist/index.htmlis excluded by!**/dist/**
📒 Files selected for processing (10)
src/components/AssetFilterManager.tsxsrc/components/BackToTop.tsxsrc/components/BulkActionToolbar.tsxsrc/components/CategorySidebar.tsxsrc/components/ReadmeModal.tsxsrc/components/ReleaseTimeline.tsxsrc/components/RepositoryCard.tsxsrc/components/RepositoryList.tsxsrc/components/SettingsPanel.tsxsrc/constants/presetFilters.ts
✅ Files skipped from review due to trivial changes (1)
- src/constants/presetFilters.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/components/BackToTop.tsx
- src/components/BulkActionToolbar.tsx
- src/components/CategorySidebar.tsx
- src/components/ReadmeModal.tsx
| const handleBulkCategorize = async (categoryName: string) => { | ||
| const selectedRepos = filteredRepositories.filter(repo => | ||
| selectedRepoIds.has(repo.id) | ||
| ); | ||
|
|
||
| const failedRepos: string[] = []; | ||
|
|
||
| for (const repo of selectedRepos) { | ||
| try { | ||
| updateRepository({ | ||
| ...repo, | ||
| custom_category: categoryName | ||
| }); | ||
| } catch (error) { | ||
| console.error(`Failed to categorize ${repo.full_name}:`, error); | ||
| failedRepos.push(repo.full_name); | ||
| } | ||
| } | ||
|
|
||
| await forceSyncToBackend(); | ||
|
|
||
| // 汇总结果显示 | ||
| const successCount = selectedRepos.length - failedRepos.length; | ||
| if (failedRepos.length > 0) { | ||
| alert(language === 'zh' | ||
| ? `成功为 ${successCount} 个仓库设置分类:${categoryName}\n\n失败 (${failedRepos.length} 个):\n${failedRepos.join('\n')}` | ||
| : `Successfully categorized ${successCount} repositories as: ${categoryName}\n\nFailed (${failedRepos.length}):\n${failedRepos.join('\n')}` | ||
| ); | ||
| } else { | ||
| alert(language === 'zh' | ||
| ? `成功为 ${successCount} 个仓库设置分类:${categoryName}` | ||
| : `Successfully categorized ${successCount} repositories as: ${categoryName}` | ||
| ); | ||
| } | ||
|
|
||
| handleDeselectAll(); | ||
| }; |
There was a problem hiding this comment.
handleBulkCategorize error handling is inconsistent.
The updateRepository call is wrapped in try/catch, but updateRepository from zustand is synchronous and won't throw. The error handling code is dead code. If you intend to catch async errors, this needs adjustment.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/RepositoryList.tsx` around lines 565 - 601, The try/catch
around updateRepository in handleBulkCategorize is ineffective because
updateRepository (from zustand) is synchronous and won’t throw; either (A)
remove the per-repo try/catch and treat updateRepository as synchronous, then
wrap await forceSyncToBackend() in a try/catch to capture backend sync errors
and report them (use failedRepos to record any sync-level failure), or (B) if
you intend per-repo async failures, make updateRepository return a Promise,
await each call (or Promise.all with mapping over filteredRepositories &
selectedRepoIds), catch rejections and push repo.full_name into failedRepos;
update successCount calculation accordingly and keep references to
handleBulkCategorize, updateRepository, forceSyncToBackend,
filteredRepositories, and selectedRepoIds when implementing the chosen fix.
| // 使用 requestAnimationFrame 更新指示器,避免闪烁 | ||
| const updateIndicator = useCallback(() => { | ||
| if (rafRef.current) { | ||
| cancelAnimationFrame(rafRef.current); | ||
| } | ||
|
|
||
| resetAIForm(); | ||
| }; | ||
|
|
||
| const handleEditAI = (config: AIConfig) => { | ||
| setAIForm({ | ||
| name: config.name, | ||
| apiType: config.apiType || 'openai', | ||
| baseUrl: config.baseUrl, | ||
| apiKey: config.apiKey, | ||
| model: config.model, | ||
| customPrompt: config.customPrompt || '', | ||
| useCustomPrompt: config.useCustomPrompt || false, | ||
| concurrency: config.concurrency || 1, | ||
| reasoningEffort: (config.reasoningEffort === 'minimal' ? 'low' : config.reasoningEffort) || '', | ||
| }); | ||
| setEditingAIId(config.id); | ||
| setShowAIForm(true); | ||
| setShowCustomPrompt(config.useCustomPrompt || false); | ||
| }; | ||
|
|
||
| const handleTestAI = async (config: AIConfig) => { | ||
| setTestingAIId(config.id); | ||
| try { | ||
| const aiService = new AIService(config, language); | ||
| const isConnected = await aiService.testConnection(); | ||
|
|
||
| if (isConnected) { | ||
| alert(t('AI服务连接成功!', 'AI service connection successful!')); | ||
| } else { | ||
| alert(t('AI服务连接失败,请检查配置。', 'AI service connection failed. Please check configuration.')); | ||
|
|
||
| rafRef.current = requestAnimationFrame(() => { | ||
| const activeButton = tabRefs.current.get(activeTab); | ||
| if (activeButton && scrollContainerRef.current) { | ||
| // 使用 offsetLeft 代替 getBoundingClientRect,避免重排导致的闪烁 | ||
| const container = scrollContainerRef.current; | ||
| const left = activeButton.offsetLeft - container.scrollLeft; | ||
| const width = activeButton.offsetWidth; | ||
|
|
||
| setIndicatorStyle({ left, width }); | ||
| } | ||
| } catch (error) { | ||
| console.error('AI test failed:', error); | ||
| alert(t('AI服务测试失败,请检查网络连接和配置。', 'AI service test failed. Please check network connection and configuration.')); | ||
| } finally { | ||
| setTestingAIId(null); | ||
| } | ||
| }; | ||
|
|
||
| const handleSaveWebDAV = () => { | ||
| const errors = WebDAVService.validateConfig(webdavForm); | ||
| if (errors.length > 0) { | ||
| alert(errors.join('\n')); | ||
| return; | ||
| } | ||
|
|
||
| const config: WebDAVConfig = { | ||
| id: editingWebDAVId || Date.now().toString(), | ||
| name: webdavForm.name, | ||
| url: webdavForm.url.replace(/\/$/, ''), // Remove trailing slash | ||
| username: webdavForm.username, | ||
| password: webdavForm.password, | ||
| path: webdavForm.path, | ||
| isActive: false, | ||
| }; | ||
|
|
||
| if (editingWebDAVId) { | ||
| updateWebDAVConfig(editingWebDAVId, config); | ||
| } else { | ||
| addWebDAVConfig(config); | ||
| } | ||
|
|
||
| resetWebDAVForm(); | ||
| }; | ||
|
|
||
| const handleEditWebDAV = (config: WebDAVConfig) => { | ||
| setWebDAVForm({ | ||
| name: config.name, | ||
| url: config.url, | ||
| username: config.username, | ||
| password: config.password, | ||
| path: config.path, | ||
| }); | ||
| setEditingWebDAVId(config.id); | ||
| setShowWebDAVForm(true); | ||
| }; | ||
|
|
||
| const handleTestWebDAV = async (config: WebDAVConfig) => { | ||
| setTestingWebDAVId(config.id); | ||
| try { | ||
| const webdavService = new WebDAVService(config); | ||
| const isConnected = await webdavService.testConnection(); | ||
| }, [activeTab]); |
There was a problem hiding this comment.
Recalculate the mobile indicator when layout changes.
updateIndicator only runs on mount, tab changes, and scroll end. If this nav is initially hidden by md:hidden/lg:hidden or the viewport is resized, the stored offsetLeft/offsetWidth can go stale and the underline can render at the wrong position until the next interaction. A ResizeObserver on scrollContainerRef would make this resilient.
Also applies to: 86-100, 118-127
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/SettingsPanel.tsx` around lines 53 - 70, updateIndicator can
become stale when the nav layout changes (e.g., hidden via md:hidden/lg:hidden
or viewport resize); attach a ResizeObserver to scrollContainerRef (and a window
'resize' fallback) to call updateIndicator whenever the container size changes,
ensuring you reuse the existing rafRef and tabRefs logic and call
setIndicatorStyle from updateIndicator; create the observer after
scrollContainerRef.current is available, start by calling updateIndicator once,
and cleanly disconnect the ResizeObserver and remove the window listener in the
component cleanup to avoid leaks.
| <div | ||
| className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" | ||
| role="dialog" | ||
| aria-modal="true" | ||
| aria-labelledby="settings-modal-title" | ||
| > | ||
| <div className="w-full max-w-5xl h-[85vh] bg-white dark:bg-gray-900 rounded-2xl shadow-2xl overflow-hidden flex flex-col"> | ||
| <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"> | ||
| <div className="flex items-center space-x-3"> | ||
| <Settings className="w-6 h-6 text-blue-600 dark:text-blue-400" /> | ||
| <h2 id="settings-modal-title" className="text-xl font-semibold text-gray-900 dark:text-white"> | ||
| {t('设置', 'Settings')} | ||
| </h2> | ||
| </div> | ||
| </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" | ||
| onClick={handleClose} | ||
| className="p-2 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" | ||
| aria-label="Close settings" | ||
| > | ||
| <Twitter className="w-5 h-5" /> | ||
| <span>Twitter</span> | ||
| <ExternalLink className="w-4 h-4" /> | ||
| <X className="w-5 h-5 text-gray-500 dark:text-gray-400" /> | ||
| </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"> | ||
| <div className="flex items-center space-x-3"> | ||
| <Bot className="w-6 h-6 text-purple-600 dark:text-purple-400" /> | ||
| <h3 className="text-lg font-semibold text-gray-900 dark:text-white"> | ||
| {t('AI服务配置', 'AI Service Configuration')} | ||
| </h3> | ||
| </div> | ||
| <button | ||
| onClick={() => setShowAIForm(true)} | ||
| className="flex items-center space-x-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors" | ||
| > | ||
| <Plus className="w-4 h-4" /> | ||
| <span>{t('添加AI配置', 'Add AI Config')}</span> | ||
| </button> | ||
| </div> | ||
|
|
||
| {/* AI Config Form */} | ||
| {showAIForm && ( | ||
| <div className="mb-6 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600"> | ||
| <h4 className="font-medium text-gray-900 dark:text-white mb-4"> | ||
| {editingAIId ? t('编辑AI配置', 'Edit AI Configuration') : t('添加AI配置', 'Add AI Configuration')} | ||
| </h4> | ||
|
|
||
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | ||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | ||
| {t('配置名称', 'Configuration Name')} * | ||
| </label> | ||
| <input | ||
| type="text" | ||
| value={aiForm.name} | ||
| onChange={(e) => setAIForm(prev => ({ ...prev, name: e.target.value }))} | ||
| 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={t('例如: OpenAI GPT-4', 'e.g., OpenAI GPT-4')} | ||
| /> | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | ||
| {t('接口格式', 'API Format')} * | ||
| </label> | ||
| <select | ||
| value={aiForm.apiType} | ||
| onChange={(e) => setAIForm(prev => ({ ...prev, apiType: e.target.value as AIApiType }))} | ||
| 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" | ||
| > | ||
| <option value="openai">OpenAI (Chat Completions)</option> | ||
| <option value="openai-responses">OpenAI (Responses)</option> | ||
| <option value="claude">Claude</option> | ||
| <option value="gemini">Gemini</option> | ||
| </select> | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | ||
| {t('API端点', 'API Endpoint')} * | ||
| </label> | ||
| <input | ||
| type="url" | ||
| value={aiForm.baseUrl} | ||
| onChange={(e) => setAIForm(prev => ({ ...prev, baseUrl: e.target.value }))} | ||
| 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={ | ||
| aiForm.apiType === 'openai' || aiForm.apiType === 'openai-responses' | ||
| ? 'https://api.openai.com/v1' | ||
| : aiForm.apiType === 'claude' | ||
| ? 'https://api.anthropic.com/v1' | ||
| : 'https://generativelanguage.googleapis.com/v1beta' | ||
| } | ||
| /> | ||
| <p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> | ||
| {t( | ||
| '只填到版本号即可(如 .../v1 或 .../v1beta),不要包含 /chat/completions、/responses、/messages 或 :generateContent', | ||
| 'Only include the version prefix (e.g. .../v1 or .../v1beta). Do not include /chat/completions, /responses, /messages, or :generateContent.' | ||
| )} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | ||
| {t('API密钥', 'API Key')} * | ||
| </label> | ||
| <input | ||
| type="password" | ||
| value={aiForm.apiKey} | ||
| onChange={(e) => setAIForm(prev => ({ ...prev, apiKey: e.target.value }))} | ||
| 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={t('输入API密钥', 'Enter API key')} | ||
| /> | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | ||
| {t('模型名称', 'Model Name')} * | ||
| </label> | ||
| <input | ||
| type="text" | ||
| value={aiForm.model} | ||
| onChange={(e) => setAIForm(prev => ({ ...prev, model: e.target.value }))} | ||
| 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="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> | ||
| <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | ||
| {t('推理强度', 'Reasoning Effort')} | ||
| </label> | ||
| <select | ||
| value={aiForm.reasoningEffort} | ||
| onChange={(e) => setAIForm(prev => ({ ...prev, reasoningEffort: e.target.value as '' | AIReasoningEffort }))} | ||
| 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" | ||
| > | ||
| <option value="">{t('默认 / 不传', 'Default / Do not send')}</option> | ||
| <option value="none">none</option> | ||
| <option value="low">low</option> | ||
| <option value="medium">medium</option> | ||
| <option value="high">high</option> | ||
| <option value="xhigh">xhigh</option> | ||
| </select> | ||
| <p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> | ||
| {t( | ||
| '仅对 OpenAI 兼容接口生效。留空时保持旧模式兼容,不额外传 reasoning。', | ||
| 'Only applies to OpenAI-compatible APIs. Leave empty to preserve legacy behavior and omit reasoning.' | ||
| )} | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Custom Prompt Section */} | ||
| <div className="mb-4"> | ||
| <div className="flex items-center justify-between mb-2"> | ||
| <label className="flex items-center space-x-2 cursor-pointer"> | ||
| <input | ||
| type="checkbox" | ||
| checked={aiForm.useCustomPrompt} | ||
| onChange={(e) => { | ||
| setAIForm(prev => ({ ...prev, useCustomPrompt: e.target.checked })); | ||
| setShowCustomPrompt(e.target.checked); | ||
| }} | ||
| className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" | ||
| /> | ||
| <span className="text-sm font-medium text-gray-700 dark:text-gray-300"> | ||
| {t('使用自定义提示词', 'Use Custom Prompt')} | ||
| </span> | ||
| </label> | ||
| {showCustomPrompt && ( | ||
| <div className="flex flex-col md:flex-row flex-1 overflow-hidden"> | ||
| {/* 侧边栏 - 桌面端 */} | ||
| <div className="hidden md:block w-64 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 overflow-y-auto"> | ||
| <nav className="p-4 space-y-1" role="tablist"> | ||
| {tabs.map((tab) => ( | ||
| <button | ||
| onClick={() => setAIForm(prev => ({ ...prev, customPrompt: getDefaultPrompt() }))} | ||
| className="text-sm text-blue-600 dark:text-blue-400 hover:underline" | ||
| key={tab.id} | ||
| onClick={() => handleTabChange(tab.id)} | ||
| role="tab" | ||
| id={`tab-${tab.id}`} | ||
| aria-selected={activeTab === tab.id} | ||
| aria-controls={`tabpanel-${tab.id}`} | ||
| className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors text-left ${ | ||
| activeTab === tab.id | ||
| ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300' | ||
| : 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700' | ||
| }`} | ||
| > | ||
| {t('使用默认模板', 'Use Default Template')} | ||
| {tab.icon} | ||
| <span className="font-medium">{tab.label}</span> | ||
| </button> | ||
| )} | ||
| </div> | ||
|
|
||
| {showCustomPrompt && ( | ||
| <div> | ||
| <textarea | ||
| value={aiForm.customPrompt} | ||
| onChange={(e) => setAIForm(prev => ({ ...prev, customPrompt: e.target.value }))} | ||
| 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 text-sm font-mono" | ||
| rows={12} | ||
| placeholder={t('输入自定义提示词...', 'Enter custom prompt...')} | ||
| /> | ||
| <div className="mt-2 text-xs text-gray-500 dark:text-gray-400"> | ||
| <p className="mb-1">{t('可用占位符:', 'Available placeholders:')}</p> | ||
| <div className="flex flex-wrap gap-2"> | ||
| <code className="px-2 py-1 bg-gray-200 dark:bg-gray-600 rounded">{'{{REPO_INFO}}'}</code> | ||
| <code className="px-2 py-1 bg-gray-200 dark:bg-gray-600 rounded">{'{{CATEGORIES_INFO}}'}</code> | ||
| <code className="px-2 py-1 bg-gray-200 dark:bg-gray-600 rounded">{'{{LANGUAGE}}'}</code> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| )} | ||
| ))} | ||
| </nav> | ||
| </div> | ||
|
|
||
| <div className="flex space-x-3"> | ||
| <button | ||
| onClick={handleSaveAI} | ||
| className="flex items-center space-x-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors" | ||
| > | ||
| <Save className="w-4 h-4" /> | ||
| <span>{t('保存', 'Save')}</span> | ||
| </button> | ||
| <button | ||
| onClick={resetAIForm} | ||
| className="flex items-center space-x-2 px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors" | ||
| > | ||
| <X className="w-4 h-4" /> | ||
| <span>{t('取消', 'Cancel')}</span> | ||
| </button> | ||
| {/* 移动端标签选择器 */} | ||
| <div className="md:hidden"> | ||
| <MobileTabNav | ||
| tabs={tabs} | ||
| activeTab={activeTab} | ||
| onTabChange={handleTabChange} | ||
| /> | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* AI Configs List */} | ||
| <div className="space-y-3"> | ||
| {aiConfigs.map(config => ( | ||
| <div | ||
| key={config.id} | ||
| className={`p-4 rounded-lg border transition-colors ${ | ||
| config.id === activeAIConfig | ||
| ? 'border-purple-300 bg-purple-50 dark:border-purple-600 dark:bg-purple-900/20' | ||
| : 'border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500' | ||
| }`} | ||
| > | ||
| <div className="flex items-center justify-between"> | ||
| <div className="flex items-center space-x-3"> | ||
| <input | ||
| type="radio" | ||
| name="activeAI" | ||
| checked={config.id === activeAIConfig} | ||
| onChange={() => setActiveAIConfig(config.id)} | ||
| className="w-4 h-4 text-purple-600 bg-gray-100 border-gray-300 focus:ring-purple-500 dark:focus:ring-purple-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" | ||
| /> | ||
| <div> | ||
| <h4 className="font-medium text-gray-900 dark:text-white"> | ||
| {config.name} | ||
| {config.useCustomPrompt && ( | ||
| <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"> | ||
| <MessageSquare className="w-3 h-3 mr-1" /> | ||
| {t('自定义提示词', 'Custom Prompt')} | ||
| </span> | ||
| )} | ||
| </h4> | ||
| <p className="text-sm text-gray-500 dark:text-gray-400"> | ||
| {(config.apiType || 'openai').toUpperCase()} • {config.baseUrl} • {config.model} • {t('并发数', 'Concurrency')}: {config.concurrency || 1} | ||
| {config.reasoningEffort ? ` • reasoning: ${config.reasoningEffort}` : ''} | ||
| </p> | ||
| {config.apiKeyStatus === 'decrypt_failed' && ( | ||
| <p className="mt-1 text-sm text-amber-600 dark:text-amber-400"> | ||
| {t( | ||
| '存储的 API Key 无法解密,请重新输入并保存该配置。', | ||
| 'The stored API key could not be decrypted. Please re-enter and save this configuration.' | ||
| )} | ||
| </p> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="flex items-center space-x-2"> | ||
| <button | ||
| onClick={() => handleTestAI(config)} | ||
| disabled={testingAIId === config.id} | ||
| className="p-2 rounded-lg bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400 hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors disabled:opacity-50" | ||
| title={t('测试连接', 'Test Connection')} | ||
| > | ||
| {testingAIId === config.id ? ( | ||
| <RefreshCw className="w-4 h-4 animate-spin" /> | ||
| ) : ( | ||
| <TestTube className="w-4 h-4" /> | ||
| )} | ||
| </button> | ||
| <button | ||
| onClick={() => handleEditAI(config)} | ||
| className="p-2 rounded-lg bg-orange-100 text-orange-600 dark:bg-orange-900 dark:text-orange-400 hover:bg-orange-200 dark:hover:bg-orange-800 transition-colors" | ||
| title={t('编辑', 'Edit')} | ||
| > | ||
| <Edit3 className="w-4 h-4" /> | ||
| </button> | ||
| <button | ||
| onClick={() => { | ||
| if (confirm(t('确定要删除这个AI配置吗?', 'Are you sure you want to delete this AI configuration?'))) { | ||
| deleteAIConfig(config.id); | ||
| } | ||
| }} | ||
| className="p-2 rounded-lg bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-800 transition-colors" | ||
| title={t('删除', 'Delete')} | ||
| > | ||
| <Trash2 className="w-4 h-4" /> | ||
| </button> | ||
| </div> | ||
| {/* 内容区域 */} | ||
| <div className="flex-1 overflow-y-auto p-6"> | ||
| <div className="max-w-3xl mx-auto"> | ||
| {renderTabContent()} | ||
| </div> | ||
| </div> | ||
| ))} | ||
|
|
||
| {aiConfigs.length === 0 && ( | ||
| <div className="text-center py-8 text-gray-500 dark:text-gray-400"> | ||
| <Bot className="w-12 h-12 mx-auto mb-3 opacity-50" /> | ||
| <p>{t('还没有配置AI服务', 'No AI services configured yet')}</p> | ||
| <p className="text-sm">{t('点击上方按钮添加AI配置', 'Click the button above to add AI configuration')}</p> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* WebDAV 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"> | ||
| <div className="flex items-center space-x-3"> | ||
| <Cloud className="w-6 h-6 text-blue-600 dark:text-blue-400" /> | ||
| <h3 className="text-lg font-semibold text-gray-900 dark:text-white"> | ||
| {t('WebDAV备份配置', 'WebDAV Backup Configuration')} | ||
| </h3> | ||
| </div> | ||
| <div className="flex items-center space-x-3"> | ||
| {lastBackup && ( | ||
| <span className="text-sm text-gray-500 dark:text-gray-400"> | ||
| {t('上次备份:', 'Last backup:')} {new Date(lastBackup).toLocaleString()} | ||
| </span> | ||
| )} | ||
| <button | ||
| onClick={() => setShowWebDAVForm(true)} | ||
| className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" | ||
| > | ||
| <Plus className="w-4 h-4" /> | ||
| <span>{t('添加WebDAV', 'Add WebDAV')}</span> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Complete the modal’s keyboard interaction model.
The dialog has ARIA metadata now, but focus is never moved into it, trapped there, or restored on close, and there is no Escape shortcut. Keyboard users can still tab into the page behind the overlay and lose context.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/SettingsPanel.tsx` around lines 332 - 397, The modal currently
lacks keyboard focus handling; update SettingsPanel to save
document.activeElement before opening, move focus into the modal (e.g., focus
the first focusable in the modal container ref), trap Tab/Shift+Tab within the
modal, close on Escape by calling handleClose, and restore focus to the saved
element on close; implement these behaviors in a useEffect tied to the modal
open state, attach keydown handlers and focus-wrap logic to the modal container
ref (the same element that renders the dialog), and ensure background content is
made inert/aria-hidden while the modal is open so users cannot tab into the page
behind it.
refactor(RepositoryList): 提取选中仓库数组为统一变量 refactor(ReadmeModal): 重构Markdown链接处理逻辑 feat(CategorySidebar): 添加文字显示动画效果 fix(SettingsPanel): 修复移动端标签页指示器位置问题 feat(AssetFilterManager): 增加按钮可访问性属性 feat(ReleaseTimeline): 改进发布说明折叠和文件列表交互
移除SettingsPanel和CategorySidebar组件中的合并冲突标记
|
@coderabbitai review the PR |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/RepositoryList.tsx (1)
835-841:⚠️ Potential issue | 🔴 CriticalUpdate
RepositoryCardto accept the new selection props.
RepositoryCardis still declared with onlyrepository,showAISummary, andsearchQueryinsrc/components/RepositoryCard.tsx:12-22. PassingisSelectedandonSelecthere will fail type-checking, and the bulk-selection UI cannot work until that component contract is expanded.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryList.tsx` around lines 835 - 841, RepositoryCard is still typed to accept only repository, showAISummary and searchQuery, but callers now pass isSelected and onSelect; update the props interface and component signature in RepositoryCard (the RepositoryCard component and its props type in src/components/RepositoryCard.tsx) to include isSelected: boolean (or optional boolean) and onSelect: (id: string, selected: boolean) => void (or optional), adjust the destructuring inside RepositoryCard to use these props (e.g., to render selection state and call onSelect when toggled), and ensure any internal handlers (checkbox/click) use repository.id when invoking onSelect so TypeScript type-checks and the bulk-selection UI works.
♻️ Duplicate comments (4)
src/components/ReadmeModal.tsx (1)
59-62:⚠️ Potential issue | 🟠 MajorResolve relative README links against a blob base, not repo root.
new URL(link, baseUrl + '/')yields URLs likehttps://github.com/{owner}/{repo}/docs/setup.md, which usually won’t open file views correctly on GitHub. Use a/blob/HEAD/(or default-branch) base for README relative paths.🔧 Suggested adjustment
- return new URL(link, baseUrl + '/').href; + return new URL(link, `${baseUrl}/blob/HEAD/`).href;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReadmeModal.tsx` around lines 59 - 62, The current relative README link resolver uses new URL(link, baseUrl + '/') which builds paths against the repo root; update the resolver in ReadmeModal.tsx to resolve relative links against a blob base instead (e.g., baseUrl + '/blob/HEAD/' or the repo default branch) so links become https://github.com/{owner}/{repo}/blob/HEAD/{path}; change the baseUrl usage where new URL(link, baseUrl + '/') is called to use the '/blob/HEAD/' segment (or compute and insert the actual default branch) before constructing the URL and keep the existing try/catch behavior for invalid URLs.src/components/ReleaseTimeline.tsx (2)
725-728:⚠️ Potential issue | 🟠 MajorMake the release header a real disclosure control.
Lines 725-728 are mouse-only today. Keyboard users cannot expand a release card, and the open/closed state is never announced.
Suggested fix
<div className="p-3 sm:p-4 cursor-pointer select-none" onClick={() => toggleReleaseExpansion(release.id)} + role="button" + tabIndex={0} + aria-expanded={isExpanded} + aria-controls={`release-panel-${release.id}`} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleReleaseExpansion(release.id); + } + }} > @@ - <div className={`overflow-hidden transition-all duration-300 ease-in-out ${ - isExpanded ? 'max-h-[5000px] opacity-100' : 'max-h-0 opacity-0' - }`}> + <div + id={`release-panel-${release.id}`} + className={`overflow-hidden transition-all duration-300 ease-in-out ${ + isExpanded ? 'max-h-[5000px] opacity-100' : 'max-h-0 opacity-0' + }`} + >Also applies to: 860-863
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 725 - 728, The release header currently uses a plain div for expansion toggling (onClick={() => toggleReleaseExpansion(release.id)}) which is not keyboard accessible or announced; replace the div with a semantic button (or add role="button", tabIndex=0, and keyboard handlers) and include proper aria attributes: set aria-expanded={isReleaseExpanded(release.id)} and aria-controls referencing the release panel id so screen readers announce open/closed state; ensure the element uses onKeyDown to trigger toggleReleaseExpansion(release.id) on Enter/Space and apply the same change to the other identical header instance that also calls toggleReleaseExpansion.
54-65:⚠️ Potential issue | 🟡 MinorDon’t treat
!classNameas “inline code”.This still misclassifies fenced code blocks without a language as inline code in
react-markdown, so multi-line blocks can lose their<pre>wrapper.Suggested fix
- code: ({ children, className }) => { - const isInline = !className; + code: ({ children, className }) => { + const isInline = !className && !String(children).includes('\n'); return isInline ? (In react-markdown 10.1.0 with remark-gfm 4.0.1, can fenced code blocks without an explicit language reach the `code` renderer without a `className`, causing `!className` to misclassify them as inline code?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 54 - 65, The current code renderer in ReleaseTimeline.tsx misclassifies fenced code blocks that lack className by using "!className" to detect inline code; switch the renderer to use the "inline" prop provided by react-markdown instead of className (update the code renderer signature from code: ({ children, className }) => ... to accept and use the inline boolean, e.g., code: ({ children, inline, className }) => and base the inline vs block decision on inline rather than !className), preserving the same inline and block JSX rendering paths.src/components/AssetFilterManager.tsx (1)
238-255:⚠️ Potential issue | 🟠 MajorExpose custom filter state as a toggle.
Line 247 still behaves like a selectable chip, but it does not expose its selected state. Right now the active/inactive state is only conveyed by color, so screen readers cannot tell which custom filters are on.
Suggested fix
- {customFilters.map(filter => ( + {customFilters.map(filter => { + const isSelected = selectedFilters.includes(filter.id); + return ( <div key={filter.id} className={`group flex items-center space-x-2 px-3 py-2 rounded-lg border transition-colors ${ - selectedFilters.includes(filter.id) + isSelected ? 'bg-blue-100 border-blue-300 text-blue-700 dark:bg-blue-900 dark:border-blue-700 dark:text-blue-300' : 'bg-gray-100 border-gray-300 text-gray-700 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600' }`} > <button + type="button" onClick={() => onFilterToggle(filter.id)} + aria-pressed={isSelected} className="flex items-center space-x-2 flex-1" > <span className="font-medium text-sm">{filter.name}</span> <span className="text-xs opacity-75 hidden lg:inline"> ({filter.keywords.join(', ')}) </span> </button> @@ </div> - ))} + )})}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/AssetFilterManager.tsx` around lines 238 - 255, The custom filter button in the customFilters map is currently only visually indicating selection; make it an accessible toggle by adding an ARIA pressed state and accessible label: on the button rendered inside the customFilters.map (the element using onFilterToggle(filter.id)), set aria-pressed={selectedFilters.includes(filter.id)} and add an aria-label or visually-hidden text that includes filter.name and its current state (e.g., "on" or "off"); keep the onClick handler (onFilterToggle) as-is so clicking toggles state, and ensure the button remains a semantic <button> so screen readers can announce it as a toggle.
🧹 Nitpick comments (1)
src/components/CategorySidebar.tsx (1)
311-342: Avoid hiding the active category in collapsed mode.
allCategories.slice(0, 8)can exclude the currently selected/custom category. When that happens, the collapsed sidebar loses both the selected-state indicator and the direct shortcut back to that filter until the user expands it again. Consider pinning the active category into the compact list, or adding an overflow affordance instead of a hard cutoff.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 311 - 342, The compact rendering currently uses allCategories.slice(0, 8) which can drop the active category and hide selected state; update the rendering logic in CategorySidebar so the displayed list always includes the selectedCategory: compute a displayedCategories array (e.g., take the first N entries, then if selectedCategory is not present push it and dedupe) and map over displayedCategories instead of allCategories.slice(0, 8); keep existing handlers (onCategorySelect, onDragOver, onDrop, dragOverCategoryId checks) and preserve ordering/unique ids so the UI still shows the selected item in collapsed mode.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 397-410: The drag/drop handlers are only on the inner <button> so
the hover toolbar overlay (sibling) intercepts pointer events and prevents
handleDropOnCategory from firing; move the drag handlers (onDragOver,
onDragLeave, onDrop) and the setDragOverCategoryId logic from the button to the
outer wrapper element (the div with className="group relative") or alternatively
add the same handlers to the toolbar overlay element so the entire category row
(including the right-side toolbar) is a single drop target; update references to
category.id, setDragOverCategoryId and handleDropOnCategory accordingly so
dropping over the toolbar still triggers handleDropOnCategory.
In `@src/components/ReadmeModal.tsx`:
- Around line 28-76: Remove the Git conflict markers and produce a single merged
implementation: keep the getBaseUrl() helper, call it inside resolveHref()
(i.e., declare const baseUrl = getBaseUrl();), and ensure resolveHref uses that
baseUrl when constructing new URL; remove any duplicate/whitespace conflict
remnants so the file compiles, and keep the exported/resolved value resolvedHref
as before. Make sure getBaseUrl, resolveHref, and resolvedHref are consistently
defined and typed (no leftover markers).
- Around line 299-300: The README rendering uses rehypeRaw without sanitization
in ReadmeModal (rehypePlugins prop), which allows XSS; import and add
rehypeSanitize (e.g., rehypeSanitize) to the rehypePlugins array alongside
rehypeRaw in the ReadmeModal component and ensure you pass any required schema
if customizing; also add "rehype-sanitize": "^6.0.0" to package.json
dependencies and run install so the module is available at runtime.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 152-168: matchesActiveFilters currently reads preset keywords from
the static PRESET_FILTERS constant so edits made via AssetFilterManager aren't
respected; modify matchesActiveFilters to use the authoritative assetFilters
collection (which contains both custom and persisted preset filters) instead of
PRESET_FILTERS when building activePresetFilters, e.g., derive active filters by
filtering assetFilters for ids in selectedFilters and then match keywords from
that result; keep the dependency array ([selectedFilters, assetFilters]) so
changes to persisted presets re-run the matcher.
- Around line 793-853: The icon-only action buttons (the file-toggle button, the
unsubscribe button that calls handleUnsubscribeRelease, and the GitHub link that
calls markReleaseAsRead) lack reliable accessible names; add explicit aria-label
attributes (using the same translated strings from t()) to each control so
screen readers receive a proper name, ensure the file-toggle's aria-label
reflects its current state (use openDropdowns.has(release.id) to choose between
the "Show Files"/"Hide Files" label), and add an appropriate aria-label for the
unsubscribe and GitHub actions (e.g., translated "Unsubscribe from releases" and
"View on GitHub") so they are accessible when icon-only.
In `@src/components/RepositoryList.tsx`:
- Around line 469-535: The bulk "ai-summary" branch doesn't toggle isLoading /
isAnalyzingRef, so the UI remains enabled and allows re-entry; before starting
the batch (just after validating activeConfig and before creating
GitHubApiService / AIService or the for-loop) set setIsLoading(true) and
isAnalyzingRef.current = true, wrap the entire batching + await
forceSyncToBackend() in a try/finally and in finally call setIsLoading(false)
and isAnalyzingRef.current = false so state is cleared even on errors; ensure
any early returns that start processing are avoided (only set busy flags once
ready to process) and keep updateRepository and forceSyncToBackend usage
unchanged.
- Around line 576-608: Remove the leftover git conflict markers and keep the
correct refactor that computes selectedRepos from filteredRepositories.filter
using selectedRepoIds; specifically, in handleBulkCategorize delete the<<<<<<<,
=======, and >>>>>>> tokens and the old for-loop that iterates
selectedRepositories, then ensure the function uses the computed selectedRepos
variable in the for (const repo of selectedRepos) loop, uses
updateRepository({...repo, custom_category: categoryName}), awaits
forceSyncToBackend(), and computes successCount = selectedRepos.length -
failedRepos.length; verify no remaining references to the old
selectedRepositories variable remain.
---
Outside diff comments:
In `@src/components/RepositoryList.tsx`:
- Around line 835-841: RepositoryCard is still typed to accept only repository,
showAISummary and searchQuery, but callers now pass isSelected and onSelect;
update the props interface and component signature in RepositoryCard (the
RepositoryCard component and its props type in
src/components/RepositoryCard.tsx) to include isSelected: boolean (or optional
boolean) and onSelect: (id: string, selected: boolean) => void (or optional),
adjust the destructuring inside RepositoryCard to use these props (e.g., to
render selection state and call onSelect when toggled), and ensure any internal
handlers (checkbox/click) use repository.id when invoking onSelect so TypeScript
type-checks and the bulk-selection UI works.
---
Duplicate comments:
In `@src/components/AssetFilterManager.tsx`:
- Around line 238-255: The custom filter button in the customFilters map is
currently only visually indicating selection; make it an accessible toggle by
adding an ARIA pressed state and accessible label: on the button rendered inside
the customFilters.map (the element using onFilterToggle(filter.id)), set
aria-pressed={selectedFilters.includes(filter.id)} and add an aria-label or
visually-hidden text that includes filter.name and its current state (e.g., "on"
or "off"); keep the onClick handler (onFilterToggle) as-is so clicking toggles
state, and ensure the button remains a semantic <button> so screen readers can
announce it as a toggle.
In `@src/components/ReadmeModal.tsx`:
- Around line 59-62: The current relative README link resolver uses new
URL(link, baseUrl + '/') which builds paths against the repo root; update the
resolver in ReadmeModal.tsx to resolve relative links against a blob base
instead (e.g., baseUrl + '/blob/HEAD/' or the repo default branch) so links
become https://github.com/{owner}/{repo}/blob/HEAD/{path}; change the baseUrl
usage where new URL(link, baseUrl + '/') is called to use the '/blob/HEAD/'
segment (or compute and insert the actual default branch) before constructing
the URL and keep the existing try/catch behavior for invalid URLs.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 725-728: The release header currently uses a plain div for
expansion toggling (onClick={() => toggleReleaseExpansion(release.id)}) which is
not keyboard accessible or announced; replace the div with a semantic button (or
add role="button", tabIndex=0, and keyboard handlers) and include proper aria
attributes: set aria-expanded={isReleaseExpanded(release.id)} and aria-controls
referencing the release panel id so screen readers announce open/closed state;
ensure the element uses onKeyDown to trigger toggleReleaseExpansion(release.id)
on Enter/Space and apply the same change to the other identical header instance
that also calls toggleReleaseExpansion.
- Around line 54-65: The current code renderer in ReleaseTimeline.tsx
misclassifies fenced code blocks that lack className by using "!className" to
detect inline code; switch the renderer to use the "inline" prop provided by
react-markdown instead of className (update the code renderer signature from
code: ({ children, className }) => ... to accept and use the inline boolean,
e.g., code: ({ children, inline, className }) => and base the inline vs block
decision on inline rather than !className), preserving the same inline and block
JSX rendering paths.
---
Nitpick comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 311-342: The compact rendering currently uses
allCategories.slice(0, 8) which can drop the active category and hide selected
state; update the rendering logic in CategorySidebar so the displayed list
always includes the selectedCategory: compute a displayedCategories array (e.g.,
take the first N entries, then if selectedCategory is not present push it and
dedupe) and map over displayedCategories instead of allCategories.slice(0, 8);
keep existing handlers (onCategorySelect, onDragOver, onDrop, dragOverCategoryId
checks) and preserve ordering/unique ids so the UI still shows the selected item
in collapsed mode.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 41b37266-1741-4a7d-8c85-7d7cec83d392
📒 Files selected for processing (7)
src/components/AssetFilterManager.tsxsrc/components/BulkActionToolbar.tsxsrc/components/CategorySidebar.tsxsrc/components/ReadmeModal.tsxsrc/components/ReleaseTimeline.tsxsrc/components/RepositoryList.tsxsrc/components/SettingsPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- src/components/BulkActionToolbar.tsx
- src/components/SettingsPanel.tsx
fix(AssetFilterManager): 为筛选按钮添加无障碍属性 feat(ReadmeModal): 引入rehype-sanitize插件增强安全性 refactor(ReleaseTimeline): 改进折叠按钮的无障碍支持 perf(RepositoryList): 优化批量AI分析流程 style(RepositoryCard): 调整选择模式下的交互逻辑 refactor(CategorySidebar): 优化分类拖放交互体验
|
@coderabbitai review the PR and edit the title |
🧩 Analysis chain
I'll update it to:
🏁 Script executed: Length of output: 297
You can update it manually! Here's the corrected title to copy-paste: Changes made:
You can edit the title by clicking the ✏️ pencil icon next to the PR title at the top of the PR page. The review has already been triggered! 🚀 ✅ Actions performedReview triggered.
|
|
@CodeRabbit Summarize this PR |
|
Tip For best results, initiate chat on the files or code changes.
PR Summary🎯 OverviewThis PR is a large feature update (~+2,300 lines net) that adds multilingual UI support, completely refactors the settings panel into modular subcomponents, introduces bulk selection and data management features, optimizes the category sidebar, and adds a back-to-top button. 📋 Changes by Area1. 🌐 Multilingual Button Support (
|
| Component | Responsibility |
|---|---|
GeneralPanel |
Language toggle, version/update checker, contact links |
AIConfigPanel |
Add/edit/delete AI service configs, connection testing |
WebDAVPanel |
Manage WebDAV configs, test connections |
BackupPanel |
Backup and restore app state via WebDAV |
BackendPanel |
Backend health check, auth verification, bidirectional sync |
CategoryPanel |
Custom category CRUD, hide/show default categories |
DataManagementPanel |
Data statistics, selective deletion per category, full data wipe with localStorage/IndexedDB cleanup |
index.ts |
Barrel export for all subcomponents |
4. 📂 Sidebar Optimization (CategorySidebar.tsx)
- Responsive layout: mobile always shows full sidebar; desktop supports collapse/expand.
- Collapsed desktop mode shows icon-only list (first 8 categories).
Ctrl/Cmd + Bkeyboard shortcut toggles sidebar state.isSidebarCollapsedstate is persisted in the Zustand store and survives page reloads.
5. ⬆️ Back-to-Top Button (BackToTop.tsx + App.tsx)
- Fixed-position button appears when scroll position exceeds 300px.
- Supports smooth scrolling, ARIA accessibility attributes, and localized tooltip text.
6. 📦 Bulk Selection & Actions (RepositoryList.tsx, BulkActionToolbar.tsx, BulkCategorizeModal.tsx)
- Multi-select repository cards with select-all / deselect-all.
- Bulk actions: unstar, bulk categorize, bulk AI summary, bulk subscribe to releases.
- Two-step confirmation flow with 3-second auto-clear for safety.
7. 🔍 Asset Filter Presets (AssetFilterManager.tsx, src/constants/presetFilters.ts)
- Introduces
PRESET_FILTERSfor Windows, macOS, Linux, Android, and Source. - Expandable filter panel with preset chips, custom filters, and a "reset presets" action.
8. 📖 README Modal (ReadmeModal.tsx)
- Fetches and renders repository README content with
ReactMarkdown+remark-gfm+rehype-raw. - Handles UTF-8 decoding, relative image/link resolution, focus management, and Escape-to-close.
9. 🎨 CSS Utilities (index.css)
.scrollbar-hide— hides scrollbars across IE, Firefox, and WebKit..touch-manipulation— optimizes touch interaction.- New animation keyframes:
slide-up,slide-down,fade-in.
10. 🗄️ Store & Type Updates (useAppStore.ts, types/index.ts)
- Added
isSidebarCollapsed: booleantoAppStateand store actions. assetFiltersnow defaults todefaultPresetFiltersinstead of an empty array.AssetFiltertype extended with optionalisPresetandiconfields.
⚠️ Notable Issues to Address Before Merging
-
Unresolved merge conflict markers in
src/components/ReadmeModal.tsxandsrc/components/RepositoryList.tsx— these<<<<<<< HEAD/=======/>>>>>>>markers will cause compilation failures. These must be resolved before the PR can be safely merged. -
BackupPanelstores masked secrets (***) — ensure restore logic correctly preserves existing secrets for masked entries to avoid data loss. -
DataManagementPanelcallswindow.location.reload()after "delete all" — this is intentional but worth confirming UX expectation with stakeholders.
📊 Scale
- New files: ~8
- Modified files: ~7
- Net additions: ~+2,300 lines
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/CategorySidebar.tsx (1)
186-205:⚠️ Potential issue | 🟠 MajorHandle sync failures in the drop path.
This path updates the repository locally and then awaits
forceSyncToBackend(), but any rejection here becomes an unhandled async failure from the drop event. That leaves the UI in an optimistic state with no recovery or user feedback if backend sync fails.🛡️ Minimal hardening
const handleDropOnCategory = async (event: React.DragEvent<HTMLDivElement>, category: Category) => { event.preventDefault(); setDragOverCategoryId(null); ... - updateRepository(nextRepo); - await forceSyncToBackend(); + try { + updateRepository(nextRepo); + await forceSyncToBackend(); + } catch (error) { + updateRepository(repository); + console.error('Failed to sync dropped category change', error); + } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 186 - 205, The drop handler handleDropOnCategory currently applies an optimistic local update via updateRepository and then awaits forceSyncToBackend without handling rejections; wrap the await call in try/catch and on failure revert the optimistic change (e.g., restore the original repository object from repositoryMap or a saved copy), reset any UI drag state (setDragOverCategoryId), and surface an error to the user (toast, alert, or processLogger) so the UI doesn't remain inconsistent; ensure you capture the original repository before creating nextRepo and use its id in the revert path so updateRepository can restore it if forceSyncToBackend throws.
♻️ Duplicate comments (1)
src/components/ReleaseTimeline.tsx (1)
152-168:⚠️ Potential issue | 🟠 MajorPreset filter edits are not respected in filtering logic.
matchesActiveFiltersstill reads preset keywords from the staticPRESET_FILTERSconstant. If users edit preset filters viaAssetFilterManager, those edits are stored inassetFilters, but this function ignores them for preset matches. This means edited preset keywords won't affect filtering results.Suggested fix to use persisted filters
const matchesActiveFilters = useCallback((linkName: string): boolean => { if (selectedFilters.length === 0) return true; const lowerLinkName = linkName.toLowerCase(); - const activeCustomFilters = assetFilters.filter(filter => selectedFilters.includes(filter.id)); - const activePresetFilters = PRESET_FILTERS.filter(filter => selectedFilters.includes(filter.id)); - - const matchesCustom = activeCustomFilters.some(filter => - filter.keywords.some(keyword => lowerLinkName.includes(keyword.toLowerCase())) - ); - - const matchesPreset = activePresetFilters.some(filter => - filter.keywords.some(keyword => lowerLinkName.includes(keyword.toLowerCase())) - ); - - return matchesCustom || matchesPreset; + // Use assetFilters which contains both custom and persisted preset filters + const activeFilters = assetFilters.filter(filter => selectedFilters.includes(filter.id)); + // Fallback to static presets only for IDs not found in assetFilters + const fallbackPresetFilters = PRESET_FILTERS.filter( + filter => + selectedFilters.includes(filter.id) && + !assetFilters.some(af => af.id === filter.id) + ); + + return [...activeFilters, ...fallbackPresetFilters].some(filter => + filter.keywords.some(keyword => lowerLinkName.includes(keyword.toLowerCase())) + ); }, [selectedFilters, assetFilters]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 152 - 168, matchesActiveFilters currently reads preset keywords from the static PRESET_FILTERS constant so user edits to presets (persisted in assetFilters) are ignored; update matchesActiveFilters to derive active filters from assetFilters (e.g., const activeFilters = assetFilters.filter(f => selectedFilters.includes(f.id))) and then test linkName against activeFilters' keywords instead of using PRESET_FILTERS, replacing the separate matchesCustom/matchesPreset checks with a single check over activeFilters; keep the function signature and dependencies ([selectedFilters, assetFilters]) intact.
🧹 Nitpick comments (4)
src/components/AssetFilterManager.tsx (2)
214-222: Consider more descriptive aria-labels for edit/delete buttons.The current
aria-label={t('编辑', 'Edit')}andaria-label={t('删除', 'Delete')}are generic. For screen reader users managing multiple filters, including the filter name would provide better context.♻️ Example improvement
<button onClick={() => handleEditFilter(preset)} className="p-0.5 rounded hover:bg-indigo-200 dark:hover:bg-indigo-800 transition-colors" title={t('编辑', 'Edit')} type="button" - aria-label={t('编辑', 'Edit')} + aria-label={t(`编辑 ${preset.name}`, `Edit ${preset.name}`)} >Also applies to: 262-279
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/AssetFilterManager.tsx` around lines 214 - 222, Update the generic aria-labels for the edit/delete buttons to include the filter name so screen readers get context; locate the buttons that call handleEditFilter(preset) and the delete handler (e.g., handleDeleteFilter) and change their aria-labels to include the preset identifier (for example using the preset.name or preset.label via the i18n helper, e.g., compose "{t('编辑','Edit')} {preset.name}"), ensuring you update both the Edit3 button block and the corresponding delete button block (lines referencing handleEditFilter and handleDeleteFilter) to use the interpolated, descriptive aria-label.
94-108: Consider batching the reset operations.The reset function calls
deleteAssetFilterandaddAssetFilterin loops, which works but could be cleaner with a single batch operation if the store supports it. This is low priority since Zustand batches synchronous updates.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/AssetFilterManager.tsx` around lines 94 - 108, The reset logic in handleResetPresets currently loops calling deleteAssetFilter and addAssetFilter for each preset (using presetFilters, deleteAssetFilter, addAssetFilter, DEFAULT_PRESET_FILTERS, selectedFilters, onFilterToggle), which should be replaced with a single batched store operation to perform deletions and additions atomically; add a store method such as resetPresetFilters(batchToDeleteIds, batchToAddFilters) or batchUpdateFilters that removes all existing preset IDs and inserts DEFAULT_PRESET_FILTERS in one update, and update selectedFilters in the same batch (or return the new selected state) so handleResetPresets simply calls that single batch method instead of iterating and toggling per item.src/components/RepositoryList.tsx (1)
564-588: Subscribe logic is correct;try/catcharoundupdateRepositoryis ineffective.The conditional check
if (!releaseSubscriptions.has(repo.id))before toggling correctly prevents unsubscribing already-subscribed repos, addressing the previous concern.However, the
try/catcharoundupdateRepository(a synchronous zustand action) is dead code—it will never throw. Consider removing it or wrapping only theawait forceSyncToBackend()call in error handling.♻️ Proposed simplification
case 'subscribe': { let successCount = 0; for (const repo of repos) { - try { - // 显式设置订阅为 true,避免误取消已订阅仓库 - const updatedRepo = { ...repo, subscribed_to_releases: true }; - updateRepository(updatedRepo); - // 只在未订阅时才调用 toggle,避免误取消 - if (!releaseSubscriptions.has(repo.id)) { - toggleReleaseSubscription(repo.id); - } - successCount++; - } catch (error) { - console.error(`Failed to subscribe ${repo.full_name}:`, error); + const updatedRepo = { ...repo, subscribed_to_releases: true }; + updateRepository(updatedRepo); + if (!releaseSubscriptions.has(repo.id)) { + toggleReleaseSubscription(repo.id); } + successCount++; } - await forceSyncToBackend(); + try { + await forceSyncToBackend(); + } catch (error) { + console.error('Failed to sync subscriptions:', error); + } alert(language === 'zh'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryList.tsx` around lines 564 - 588, The try/catch around updateRepository is unnecessary because updateRepository is a synchronous zustand action and won't throw; remove the outer try/catch and instead handle only async/throwing operations. Specifically, stop wrapping the entire per-repo block in try/catch, call updateRepository(updatedRepo) directly, keep the conditional toggleReleaseSubscription(repo.id) check (using releaseSubscriptions.has), and wrap the async forceSyncToBackend() (and toggleReleaseSubscription if it is async) in a single try/catch to log failures and still report successCount correctly; adjust successCount to increment only on successful subscribe operations.src/components/ReleaseTimeline.tsx (1)
787-793: Consider computing filtered links once per release card.The same filtering operation
downloadLinks.filter(link => matchesActiveFilters(link.name))is performed three times within each release card (lines 788, 902, 919). Computing this once at the start of the card render would be cleaner.Suggested refactor
Add this near line 714, after computing
downloadLinks:const downloadLinks = getDownloadLinks(release); const isUnread = isReleaseUnread(release.id); const isExpanded = expandedReleases.has(release.id); const isFullContent = fullContentReleases.has(release.id); const isReleaseNotesCollapsed = collapsedReleaseNotes.has(release.id); + const filteredDownloadLinks = downloadLinks.filter(link => matchesActiveFilters(link.name)); + const showFilteredCount = selectedFilters.length > 0 && filteredDownloadLinks.length !== downloadLinks.length;Then replace the IIFEs with direct references to
filteredDownloadLinksandshowFilteredCount.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 787 - 793, Compute the filtered links once per release card instead of repeating downloadLinks.filter(...) multiple times: inside the ReleaseTimeline card render (after downloadLinks is computed) create a const filteredDownloadLinks = downloadLinks.filter(link => matchesActiveFilters(link.name)) and a const showFilteredCount = selectedFilters.length > 0 && filteredDownloadLinks.length !== downloadLinks.length, then replace the three IIFEs that call downloadLinks.filter(...) with direct uses of filteredDownloadLinks and showFilteredCount (references: downloadLinks, matchesActiveFilters, filteredDownloadLinks, showFilteredCount).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 49-58: The effect that toggles showText based on
isSidebarCollapsed (the useEffect using isSidebarCollapsed and setShowText) must
clear any existing timeout before creating a new one to avoid stale callbacks;
introduce a ref (e.g., showTextTimerRef) to hold the timeout id, call
clearTimeout(showTextTimerRef.current) before setting a new setTimeout and
assign the id to the ref, and also clear it in the effect cleanup and on
unmount. Apply the same fix to the other effect controlling showText (the second
useEffect around lines 70-81) so both places cancel previous timers before
scheduling new ones.
- Around line 250-276: The category buttons in CategorySidebar.tsx only indicate
selection visually; update the button elements (the one using onCategorySelect
and isSelected and the other similar buttons at the other locations) to expose
state to assistive tech by adding semantic ARIA attributes—set
aria-pressed={isSelected} (or aria-current="true" when appropriate for
navigation) and ensure the title/label remains accessible; keep onCategorySelect
handler and visual classes unchanged but add aria-pressed={isSelected} (and
aria-current={isSelected ? 'true' : undefined} if this represents the active
view) so screen readers know which category is active.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 433-450: getTruncatedBody currently falls back to
body.substring(0, maxLength) which can cut through markdown tokens and produce
invalid markup; change the fallback to search backwards from maxLength to a safe
cut point (preferably last whitespace/newline or last occurrence of closing
markdown tokens like ')', ']', '`', or '*'/'_' pair boundaries) and trim there,
or alternatively parse with a markdown-aware tokenizer to truncate at a token
boundary; update getTruncatedBody to prefer paragraph/line breaks, then if too
short scan backward from index maxLength to a safe breakpoint
(whitespace/newline or closing punctuation/markdown delimiter) before appending
'...'.
- Around line 54-65: The current custom render for the Markdown "code" component
in ReleaseTimeline.tsx relies on the removed inline prop and should be updated
for react-markdown v10: change the renderer to inspect the AST node (use
node?.parent?.tagName === 'pre' to detect block code) or split into separate
overrides for "pre" and "code" per the migration guide; update the component
signature to accept the node prop (e.g., ({ node, className, children })) and
use node?.parent?.tagName === 'pre' to choose the block <pre> rendering path (or
register a dedicated "pre" renderer that wraps a "code" renderer) so inline vs
block is detected correctly.
---
Outside diff comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 186-205: The drop handler handleDropOnCategory currently applies
an optimistic local update via updateRepository and then awaits
forceSyncToBackend without handling rejections; wrap the await call in try/catch
and on failure revert the optimistic change (e.g., restore the original
repository object from repositoryMap or a saved copy), reset any UI drag state
(setDragOverCategoryId), and surface an error to the user (toast, alert, or
processLogger) so the UI doesn't remain inconsistent; ensure you capture the
original repository before creating nextRepo and use its id in the revert path
so updateRepository can restore it if forceSyncToBackend throws.
---
Duplicate comments:
In `@src/components/ReleaseTimeline.tsx`:
- Around line 152-168: matchesActiveFilters currently reads preset keywords from
the static PRESET_FILTERS constant so user edits to presets (persisted in
assetFilters) are ignored; update matchesActiveFilters to derive active filters
from assetFilters (e.g., const activeFilters = assetFilters.filter(f =>
selectedFilters.includes(f.id))) and then test linkName against activeFilters'
keywords instead of using PRESET_FILTERS, replacing the separate
matchesCustom/matchesPreset checks with a single check over activeFilters; keep
the function signature and dependencies ([selectedFilters, assetFilters])
intact.
---
Nitpick comments:
In `@src/components/AssetFilterManager.tsx`:
- Around line 214-222: Update the generic aria-labels for the edit/delete
buttons to include the filter name so screen readers get context; locate the
buttons that call handleEditFilter(preset) and the delete handler (e.g.,
handleDeleteFilter) and change their aria-labels to include the preset
identifier (for example using the preset.name or preset.label via the i18n
helper, e.g., compose "{t('编辑','Edit')} {preset.name}"), ensuring you update
both the Edit3 button block and the corresponding delete button block (lines
referencing handleEditFilter and handleDeleteFilter) to use the interpolated,
descriptive aria-label.
- Around line 94-108: The reset logic in handleResetPresets currently loops
calling deleteAssetFilter and addAssetFilter for each preset (using
presetFilters, deleteAssetFilter, addAssetFilter, DEFAULT_PRESET_FILTERS,
selectedFilters, onFilterToggle), which should be replaced with a single batched
store operation to perform deletions and additions atomically; add a store
method such as resetPresetFilters(batchToDeleteIds, batchToAddFilters) or
batchUpdateFilters that removes all existing preset IDs and inserts
DEFAULT_PRESET_FILTERS in one update, and update selectedFilters in the same
batch (or return the new selected state) so handleResetPresets simply calls that
single batch method instead of iterating and toggling per item.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 787-793: Compute the filtered links once per release card instead
of repeating downloadLinks.filter(...) multiple times: inside the
ReleaseTimeline card render (after downloadLinks is computed) create a const
filteredDownloadLinks = downloadLinks.filter(link =>
matchesActiveFilters(link.name)) and a const showFilteredCount =
selectedFilters.length > 0 && filteredDownloadLinks.length !==
downloadLinks.length, then replace the three IIFEs that call
downloadLinks.filter(...) with direct uses of filteredDownloadLinks and
showFilteredCount (references: downloadLinks, matchesActiveFilters,
filteredDownloadLinks, showFilteredCount).
In `@src/components/RepositoryList.tsx`:
- Around line 564-588: The try/catch around updateRepository is unnecessary
because updateRepository is a synchronous zustand action and won't throw; remove
the outer try/catch and instead handle only async/throwing operations.
Specifically, stop wrapping the entire per-repo block in try/catch, call
updateRepository(updatedRepo) directly, keep the conditional
toggleReleaseSubscription(repo.id) check (using releaseSubscriptions.has), and
wrap the async forceSyncToBackend() (and toggleReleaseSubscription if it is
async) in a single try/catch to log failures and still report successCount
correctly; adjust successCount to increment only on successful subscribe
operations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8e5783a2-8678-495e-b576-6600c04eda66
⛔ Files ignored due to path filters (2)
dist/index.htmlis excluded by!**/dist/**package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
package.jsonsrc/components/AssetFilterManager.tsxsrc/components/CategorySidebar.tsxsrc/components/ReadmeModal.tsxsrc/components/ReleaseTimeline.tsxsrc/components/RepositoryCard.tsxsrc/components/RepositoryList.tsx
✅ Files skipped from review due to trivial changes (1)
- package.json
🚧 Files skipped from review as they are similar to previous changes (2)
- src/components/RepositoryCard.tsx
- src/components/ReadmeModal.tsx
修复 Markdown 代码块渲染逻辑,正确处理内联代码和代码块。优化侧边栏组件,使用 useRef 管理定时器并添加清理逻辑防止内存泄漏。同时改进仓库拖拽分类的错误处理,失败时恢复原始状态并提示用户。
|
@CodeRabbit Summarize and review PR |
|
PR
|
| Component | Responsibility |
|---|---|
GeneralPanel |
Language toggle, update checker, contact links |
AIConfigPanel |
AI service config CRUD + connection testing |
WebDAVPanel |
WebDAV config management + connection testing |
BackupPanel |
Backup/restore via WebDAV |
BackendPanel |
Backend health check, auth, bidirectional sync |
CategoryPanel |
Custom category CRUD, default category visibility |
DataManagementPanel |
Data statistics, selective deletion, full data wipe |
index.ts |
Barrel export for all subcomponents |
4. 📂 Sidebar Optimization (CategorySidebar.tsx)
- Responsive layout: mobile full sidebar; desktop collapse/expand.
- Collapsed mode shows icon-only (first 8 categories).
Ctrl/Cmd + Bkeyboard shortcut to toggle sidebar.isSidebarCollapsedpersisted in Zustand store.
5. ⬆️ Back-to-Top Button (BackToTop.tsx + App.tsx)
- Fixed-position button visible when
scrollY > 300px. - Smooth scrolling, ARIA attributes, localized tooltip.
6. 📦 Bulk Selection & Actions (RepositoryList.tsx, BulkActionToolbar.tsx, BulkCategorizeModal.tsx)
- Multi-select cards with select-all/deselect-all.
- Bulk actions: unstar, categorize, AI summary, subscribe to releases.
- Two-step confirmation with 3-second auto-clear for safety.
7. 🔍 Asset Filter Presets (AssetFilterManager.tsx, presetFilters.ts)
PRESET_FILTERSfor Windows, macOS, Linux, Android, Source.- Expandable filter panel with chips and reset-presets action.
8. 📖 README Modal (ReadmeModal.tsx)
- Fetches/renders README via
ReactMarkdown+remark-gfm+rehype-sanitize. - UTF-8 decoding, relative link/image resolution, focus management.
9. 🎨 CSS Utilities (index.css)
.scrollbar-hide,.touch-manipulation.- New animations:
slide-up,slide-down,fade-in.
10. 🗄️ Store & Type Updates
isSidebarCollapsedadded toAppStateand persisted store.AssetFilterextended with optionalisPresetandicon.
⚠️ Notable Issues to Address Before Merging
- Unresolved merge conflict markers —
src/components/ReadmeModal.tsxandsrc/components/RepositoryList.tsxlikely contain<<<<<<< HEAD/=======/>>>>>>>markers that will cause compilation failures and must be resolved. BackupPanelmasked secrets (***) — verify that restore logic correctly preserves existing secrets for masked entries to prevent credential data loss.DataManagementPanelwindow.location.reload()— intentional after "delete all", but worth confirming the UX expectation is correct.
The full code review has been triggered! 🚀
✅ Actions performed
Review triggered.
Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/components/CategorySidebar.tsx (2)
293-319:⚠️ Potential issue | 🟡 MinorExpose the active category state semantically.
These buttons still announce as plain buttons, so screen readers cannot tell which category is currently selected in any layout. Add
aria-pressed={isSelected}(or the appropriatearia-current) in all three render paths.♿ Minimal fix
<button onClick={() => onCategorySelect(category.id)} + aria-pressed={isSelected} className={`relative flex min-w-[140px] items-center justify-between px-3 py-2.5 rounded-lg text-left transition-colors ${Apply the same addition to the collapsed and expanded desktop category buttons as well.
Also applies to: 385-398, 472-506
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 293 - 319, In CategorySidebar.tsx, the category buttons (the button that calls onCategorySelect(category.id) and the collapsed/expanded desktop category buttons referenced by isSelected) don't expose selection state to assistive tech; add aria-pressed={isSelected} (or aria-current="true"/"false" if you prefer semantic current-state) to every rendered <button> for categories — include the mobile/main button that uses onCategorySelect(category.id) and the collapsed and expanded desktop variants (the other two render paths around lines noted) so screen readers can announce the active category.
55-65:⚠️ Potential issue | 🟡 MinorCancel the pending
showTexttimer before scheduling a new toggle.A collapse triggered during the first 200ms after expand still leaves
showTextTimerRefalive, so the old callback can flip labels back on before the delayed collapse finishes. Clear both timer refs at the start oftoggleSidebar().♻️ Minimal fix
const toggleSidebar = useCallback(() => { - // 清除之前的定时器 + // 清除之前的定时器 + if (showTextTimerRef.current) { + clearTimeout(showTextTimerRef.current); + showTextTimerRef.current = null; + } if (toggleSidebarTimerRef.current) { clearTimeout(toggleSidebarTimerRef.current); + toggleSidebarTimerRef.current = null; } if (isSidebarCollapsed) { // 展开侧栏:先展开,再显示文字 setSidebarCollapsed(false); toggleSidebarTimerRef.current = setTimeout(() => setShowText(true), 200); // 200ms 后显示文字,配合动效Also applies to: 83-98
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 55 - 65, The pending showText timer can fire after a quick collapse/expand and flip labels unexpectedly; update toggleSidebar() to clear any existing timers before scheduling new ones by calling clearTimeout(showTextTimerRef.current) and clearTimeout(hideTextTimerRef.current) and setting those refs to null, and similarly ensure the useEffect that sets showTextTimerRef also clears previous timers before assigning a new setTimeout; reference showTextTimerRef, hideTextTimerRef, setShowText and toggleSidebar for where to apply the changes (also apply the same pattern to the other block around the 83-98 region).
🧹 Nitpick comments (2)
src/components/ReleaseTimeline.tsx (2)
264-267: MemoizesubscribedReleasesto avoid unnecessary recalculations.
subscribedReleasescreates a new array reference on every render. Since it's used as an input to thefilteredReleasesuseMemo (line 296), this causes the expensive filtering/sorting logic to re-run even when the underlying data hasn't changed.♻️ Suggested fix
- // Filter releases for subscribed repositories - const subscribedReleases = releases.filter(release => - releaseSubscriptions.has(release.repository.id) - ); + // Filter releases for subscribed repositories + const subscribedReleases = useMemo(() => + releases.filter(release => releaseSubscriptions.has(release.repository.id)), + [releases, releaseSubscriptions] + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 264 - 267, subscribedReleases is recreated on every render causing downstream useMemo (filteredReleases) to re-run; memoize subscribedReleases using React.useMemo so it only recalculates when its inputs change (releases or releaseSubscriptions). Update the creation of subscribedReleases to use useMemo and reference the same variables (releases, releaseSubscriptions) so filteredReleases sees a stable array reference unless the underlying data actually changes.
825-831: Consolidate repeated file count calculation.The filtered file count logic is duplicated at lines 825-831 and 939-945. Consider computing
filteredLinksonce per release card and reusing the value.♻️ Suggested approach
Compute
filteredLinksat the start of the release card render (after line 747):const downloadLinks = getDownloadLinks(release); + const filteredLinks = downloadLinks.filter(link => matchesActiveFilters(link.name)); + const filteredCount = filteredLinks.length; const isUnread = isReleaseUnread(release.id);Then reuse
filteredLinksandfilteredCountthroughout the card instead of recalculating via IIFEs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 825 - 831, Extract the filtered download links calculation into a single variable at the start of the release card render and reuse it instead of recomputing in IIFEs: compute const filteredLinks = downloadLinks.filter(link => matchesActiveFilters(link.name)) and const filteredCount = filteredLinks.length once (after the release card begins, e.g., after the section where downloadLinks is available) and then replace both inline IIFEs that reference downloadLinks/filtering (the occurrences around the current expressions using matchesActiveFilters, selectedFilters, filteredCount) to use these new variables so the file count is computed only once per card.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/ReleaseTimeline.tsx`:
- Around line 1009-1022: The release notes toggle button lacks accessibility
attributes: update the button in ReleaseTimeline (the onClick handler using
toggleReleaseNotes(release.id, e) and the isReleaseNotesCollapsed state) to
include aria-expanded set to the boolean isReleaseNotesCollapsed and
aria-controls referencing a unique id for the associated content; add a matching
id to the content container (e.g., `release-notes-${release.id}`) so
aria-controls points to it, and optionally add role="region" or aria-labelledby
on that content div to improve screen-reader semantics.
---
Duplicate comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 293-319: In CategorySidebar.tsx, the category buttons (the button
that calls onCategorySelect(category.id) and the collapsed/expanded desktop
category buttons referenced by isSelected) don't expose selection state to
assistive tech; add aria-pressed={isSelected} (or aria-current="true"/"false" if
you prefer semantic current-state) to every rendered <button> for categories —
include the mobile/main button that uses onCategorySelect(category.id) and the
collapsed and expanded desktop variants (the other two render paths around lines
noted) so screen readers can announce the active category.
- Around line 55-65: The pending showText timer can fire after a quick
collapse/expand and flip labels unexpectedly; update toggleSidebar() to clear
any existing timers before scheduling new ones by calling
clearTimeout(showTextTimerRef.current) and
clearTimeout(hideTextTimerRef.current) and setting those refs to null, and
similarly ensure the useEffect that sets showTextTimerRef also clears previous
timers before assigning a new setTimeout; reference showTextTimerRef,
hideTextTimerRef, setShowText and toggleSidebar for where to apply the changes
(also apply the same pattern to the other block around the 83-98 region).
---
Nitpick comments:
In `@src/components/ReleaseTimeline.tsx`:
- Around line 264-267: subscribedReleases is recreated on every render causing
downstream useMemo (filteredReleases) to re-run; memoize subscribedReleases
using React.useMemo so it only recalculates when its inputs change (releases or
releaseSubscriptions). Update the creation of subscribedReleases to use useMemo
and reference the same variables (releases, releaseSubscriptions) so
filteredReleases sees a stable array reference unless the underlying data
actually changes.
- Around line 825-831: Extract the filtered download links calculation into a
single variable at the start of the release card render and reuse it instead of
recomputing in IIFEs: compute const filteredLinks = downloadLinks.filter(link =>
matchesActiveFilters(link.name)) and const filteredCount = filteredLinks.length
once (after the release card begins, e.g., after the section where downloadLinks
is available) and then replace both inline IIFEs that reference
downloadLinks/filtering (the occurrences around the current expressions using
matchesActiveFilters, selectedFilters, filteredCount) to use these new variables
so the file count is computed only once per card.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: afc1fd1a-aaa6-4c22-9373-2ec32e573a66
📒 Files selected for processing (2)
src/components/CategorySidebar.tsxsrc/components/ReleaseTimeline.tsx
- 在 AppState 中新增 categoryOrder 和 collapsedSidebarCategoryCount 字段 - 实现分类拖拽排序功能,支持上下移动、置顶置底操作 - 优化侧边栏折叠状态下的分类显示逻辑 - 新增退出多选模式时的卡片动画效果 - 改进批量操作工具栏的交互体验 - 更新持久化存储版本至 v2 并添加迁移逻辑
|
大概就这些了 |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
|
@CodeRabbit Summarize PR |
|
Tip For best results, initiate chat on the files or code changes.
PR
|
| Component | Responsibility |
|---|---|
GeneralPanel |
Language toggle, update checker, contact links |
AIConfigPanel |
AI service config CRUD + connection testing |
WebDAVPanel |
WebDAV config management + connection testing |
BackupPanel |
Backup/restore via WebDAV (masked secrets for API keys/passwords) |
BackendPanel |
Backend health check, auth verification, bidirectional data sync |
CategoryPanel |
Custom category CRUD, default category show/hide |
DataManagementPanel |
Data statistics, selective deletion per data type, full data wipe |
index.ts |
Barrel export for all subcomponents |
4. 📂 Sidebar Optimization (CategorySidebar.tsx)
- Responsive layout: mobile always shows full sidebar; desktop supports collapse/expand.
- Collapsed mode shows icon-only view (first 8 categories).
Ctrl/Cmd + Bkeyboard shortcut to toggle sidebar.isSidebarCollapsedpersisted in Zustand store across reloads.
5. ⬆️ Back-to-Top Button (BackToTop.tsx + App.tsx)
- Fixed-position button visible when
scrollY > 300px. - Smooth scrolling, ARIA accessibility attributes, and localized tooltip text.
6. 📦 Bulk Selection & Actions (RepositoryList.tsx, BulkActionToolbar.tsx, BulkCategorizeModal.tsx)
- Multi-select repository cards with select-all/deselect-all.
- Bulk actions: unstar, categorize, AI summary, subscribe to releases.
- Two-step confirmation with 3-second auto-clear for safety.
7. 🔍 Asset Filter Presets (AssetFilterManager.tsx, presetFilters.ts)
PRESET_FILTERSfor Windows, macOS, Linux, Android, and Source.- Expandable filter panel with preset chips and reset-presets action.
8. 📖 README Modal (ReadmeModal.tsx)
- Fetches and renders README via
ReactMarkdown+remark-gfm+rehype-sanitize. - UTF-8 decoding, relative link/image resolution, focus management, Escape-to-close.
9. 🎨 CSS Utilities (index.css)
.scrollbar-hide,.touch-manipulation.- New animation keyframes:
slide-up,slide-down,fade-in.
10. 🗄️ Store & Type Updates (useAppStore.ts, types/index.ts)
isSidebarCollapsed: booleanadded toAppStateand persisted store.AssetFilterextended with optionalisPresetandiconfields.assetFiltersnow defaults todefaultPresetFiltersinstead of an empty array.
⚠️ Issues to Address Before Merging
- Unresolved merge conflict markers —
src/components/ReadmeModal.tsxandsrc/components/RepositoryList.tsxmay contain<<<<<<< HEAD/=======/>>>>>>>markers that will cause compilation failures and must be resolved. BackupPanelmasked secrets (***) — verify that restore logic correctly preserves existing credentials for masked entries to prevent data loss.DataManagementPanelcallswindow.location.reload()— intentional after "delete all," but confirm this is the expected UX behavior.
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/store/useAppStore.ts (1)
105-130:⚠️ Potential issue | 🔴 CriticalAdd the new sidebar fields to
AppStateas well.This store now persists and exposes
categoryOrder,collapsedSidebarCategoryCount, andisSidebarCollapsed, but the providedsrc/types/index.tssnippet still omits them fromAppState. That makes thisPick<AppState, ...>and the store shape drift apart, which will break TypeScript consumers/builds.Also applies to: 302-312
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/store/useAppStore.ts` around lines 105 - 130, The AppState type is missing the new sidebar fields referenced by PersistedAppState (categoryOrder, collapsedSidebarCategoryCount, isSidebarCollapsed), causing a type drift; update the AppState definition to include these three properties with appropriate types (e.g., categoryOrder: string[] or the existing category id type, collapsedSidebarCategoryCount: number, isSidebarCollapsed: boolean) so that Pick<AppState, ...> and PersistedAppState align; ensure any related usages (e.g., PersistedAppState and useAppStore) compile against the updated AppState shape.src/components/CategorySidebar.tsx (1)
183-208:⚠️ Potential issue | 🟠 MajorHandle backend sync failures for delete/hide the same way drag-drop does.
These flows update local category state first and then call
forceSyncToBackend(), but a rejected sync is never caught or reverted. That leaves the sidebar and backend silently out of sync for destructive actions, while the drag-drop path already restores local state on failure.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 183 - 208, The delete and hide flows (handleDeleteCategory and handleHideDefaultCategory) call deleteCustomCategory/hideDefaultCategory then await forceSyncToBackend() without handling rejection; wrap the await in try/catch and on failure revert the local change (e.g., restore the removed/hidden category using the same rollback approach used by the drag-drop path) and surface the error to the user; specifically, capture previous category state before calling deleteCustomCategory or hideDefaultCategory, call the mutation, then await forceSyncToBackend() inside try/catch and in the catch block restore the previous state (or re-fetch categories) and show an error notification so UI and backend remain consistent.
♻️ Duplicate comments (7)
src/components/settings/DataManagementPanel.tsx (1)
95-98:⚠️ Potential issue | 🟠 MajorDo not wipe generic
zustand*keys here.Using
'zustand'as an app-owned prefix is still too broad on a shared origin and can remove other apps’ persisted state. Keep this wipe limited to this app’s explicit keys/prefixes only.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/DataManagementPanel.tsx` around lines 95 - 98, The APP_LOCALSTORAGE_PREFIXES array currently includes the broad prefix 'zustand', which risks wiping other apps' persisted state; change APP_LOCALSTORAGE_PREFIXES to only contain explicit app-owned prefixes (e.g. keep 'github-stars-manager' and remove 'zustand' or replace it with a narrow, app-specific key like 'zustand-github-stars-manager' or a whitelist of exact keys), and ensure any code that clears storage (using APP_LOCALSTORAGE_PREFIXES) only matches those explicit prefixes or exact keys to avoid deleting unrelated origins' data.src/components/CategorySidebar.tsx (1)
298-308:⚠️ Potential issue | 🟡 MinorExpose the active category state semantically in every layout.
The selected category is still visual-only here. Add
aria-pressed={isSelected}oraria-currentto the mobile, collapsed-desktop, and expanded-desktop category buttons so assistive tech can announce which category is active.Also applies to: 390-401, 477-487
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 298 - 308, The category buttons in CategorySidebar.tsx are only visually indicating selection; add semantic state so screen readers can announce the active category by adding aria-pressed={isSelected} (or aria-current={isSelected && category.id === 'all' ? 'page' : undefined} as appropriate) to each category <button> that uses onCategorySelect and isSelected (apply the same change in the three button locations: mobile, collapsed-desktop, expanded-desktop where the button uses onCategorySelect, category.id, category.name and t) so assistive tech receives the active-state information.src/components/settings/WebDAVPanel.tsx (1)
61-72:⚠️ Potential issue | 🟠 MajorPreserve
isActivewhen editing an existing WebDAV config.The edit path still rebuilds the config with
isActive: falseand sends it toupdateWebDAVConfig, so saving an active entry deactivates it. Look up the existing config first and update only the mutable fields while keeping its currentisActivevalue.♻️ Minimal fix
- const config: WebDAVConfig = { - id: editingId || Date.now().toString(), - name: form.name, - url: form.url.replace(/\/$/, ''), - username: form.username, - password: form.password, - path: form.path, - isActive: false, - }; - if (editingId) { - updateWebDAVConfig(editingId, config); + const existingConfig = webdavConfigs.find(c => c.id === editingId); + if (existingConfig) { + updateWebDAVConfig(editingId, { + name: form.name, + url: form.url.replace(/\/$/, ''), + username: form.username, + password: form.password, + path: form.path, + isActive: existingConfig.isActive, + }); + } } else { + const config: WebDAVConfig = { + id: Date.now().toString(), + name: form.name, + url: form.url.replace(/\/$/, ''), + username: form.username, + password: form.password, + path: form.path, + isActive: false, + }; addWebDAVConfig(config); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/WebDAVPanel.tsx` around lines 61 - 72, When saving while editing (editingId set), the code unconditionally sets isActive: false on the new config and passes it to updateWebDAVConfig; instead, find the existing WebDAVConfig for that editingId (e.g., via webdavConfigs.find or a getter used in this component), read its current isActive value, and reuse that value when building the config object before calling updateWebDAVConfig(editingId, config); keep the current behavior of setting isActive: false only when creating a new config (no editingId).src/components/ReleaseTimeline.tsx (3)
155-171:⚠️ Potential issue | 🟠 MajorUse persisted preset filters as the source of truth.
Lines 159-167 still combine
assetFilterswith the staticPRESET_FILTERSconstant. Edited preset keywords from the filter manager will be ignored here, so release visibility, file counts, and download lists can drift from the saved configuration.🛠️ Proposed fix
const matchesActiveFilters = useCallback((linkName: string): boolean => { if (selectedFilters.length === 0) return true; const lowerLinkName = linkName.toLowerCase(); - const activeCustomFilters = assetFilters.filter(filter => selectedFilters.includes(filter.id)); - const activePresetFilters = PRESET_FILTERS.filter(filter => selectedFilters.includes(filter.id)); - - const matchesCustom = activeCustomFilters.some(filter => - filter.keywords.some(keyword => lowerLinkName.includes(keyword.toLowerCase())) - ); - - const matchesPreset = activePresetFilters.some(filter => - filter.keywords.some(keyword => lowerLinkName.includes(keyword.toLowerCase())) - ); - - return matchesCustom || matchesPreset; + const activeFilters = assetFilters.filter(filter => selectedFilters.includes(filter.id)); + const fallbackPresetFilters = PRESET_FILTERS.filter( + filter => + selectedFilters.includes(filter.id) && + !activeFilters.some(active => active.id === filter.id) + ); + + return [...activeFilters, ...fallbackPresetFilters].some(filter => + filter.keywords.some(keyword => lowerLinkName.includes(keyword.toLowerCase())) + ); }, [selectedFilters, assetFilters]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 155 - 171, The matchesActiveFilters function is incorrectly using the static PRESET_FILTERS constant so edited preset keywords are ignored; update the logic to treat persisted filters in assetFilters as the single source of truth — compute activeCustomFilters and activePresetFilters by filtering assetFilters (e.g., by a preset flag or by id membership in selectedFilters) instead of referencing PRESET_FILTERS, then run the same keyword matching against those filtered assetFilters within matchesActiveFilters.
846-880:⚠️ Potential issue | 🟡 MinorExpose the downloads panel as a disclosure, not a pressed toggle.
These buttons show and hide a region, but they still only expose
aria-label/aria-pressed. Screen readers needaria-expandedandaria-controls, plus a stableidon the controlled panel.♿ Proposed fix
<button @@ title={openDropdowns.has(release.id) ? t('隐藏文件', 'Hide Files') : t('显示文件', 'Show Files')} aria-label={openDropdowns.has(release.id) ? t('隐藏文件', 'Hide Files') : t('显示文件', 'Show Files')} - aria-pressed={openDropdowns.has(release.id)} + aria-expanded={openDropdowns.has(release.id)} + aria-controls={`download-list-${release.id}`} > @@ <button onClick={(e) => toggleDropdown(release.id, e)} @@ }`} + aria-expanded={openDropdowns.has(release.id)} + aria-controls={`download-list-${release.id}`} > @@ - <div className={`overflow-hidden transition-all duration-300 ${ + <div + id={`download-list-${release.id}`} + className={`overflow-hidden transition-all duration-300 ${ openDropdowns.has(release.id) ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0' }`}>Also applies to: 929-954
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 846 - 880, The file-toggle button currently uses aria-pressed and aria-label only; change it to a proper disclosure by replacing aria-pressed with aria-expanded (bound to openDropdowns.has(release.id)) and adding aria-controls that points to a stable id for the files panel (e.g. `files-panel-${release.id}`), and ensure the controlled panel element (the collapsible file list rendered elsewhere) has that same id and role="region" (or appropriate landmark) so screen readers can associate the button with the panel; update references in the expand/collapse logic (setOpenDropdowns, toggleReleaseExpansion, openDropdowns, and the element that renders the file panel) to use the stable id but do not change the existing expansion semantics.
1009-1026:⚠️ Potential issue | 🟡 MinorWire the release-notes toggle to its content region.
The release-notes button still has no
aria-expanded/aria-controls, and the collapsible container has no matchingid. Assistive tech cannot tell whether that section is open.♿ Proposed fix
<button onClick={(e) => toggleReleaseNotes(release.id, e)} className="w-full flex items-center justify-between mb-3 group" + aria-expanded={!isReleaseNotesCollapsed} + aria-controls={`release-notes-content-${release.id}`} > @@ - <div className={`overflow-hidden transition-all duration-300 ease-in-out ${ + <div + id={`release-notes-content-${release.id}`} + className={`overflow-hidden transition-all duration-300 ease-in-out ${ isReleaseNotesCollapsed ? 'max-h-0 opacity-0' : 'max-h-[5000px] opacity-100' }`}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 1009 - 1026, Add accessible wiring between the toggle button and the collapsible region: on the button (used in toggleReleaseNotes(release.id, e)) add aria-controls and aria-expanded (e.g. aria-controls={`release-notes-${release.id}`} and aria-expanded={!isReleaseNotesCollapsed}) and ensure the collapsible div has a matching id (id={`release-notes-${release.id}`}); keep the existing visual behavior using isReleaseNotesCollapsed for the state and update aria-expanded to reflect open/closed accordingly so assistive tech can determine the section state.src/components/settings/BackupPanel.tsx (1)
111-113:⚠️ Potential issue | 🟡 MinorHandle
downloadFile()returning no content explicitly.Lines 111-113 still fall through silently when
downloadFile()returnsnullor an empty payload, so the restore spinner stops without any user feedback.🛠️ Proposed fix
const latestBackup = backupFiles.sort().reverse()[0]; const backupContent = await webdavService.downloadFile(latestBackup); - - if (backupContent) { - const backupData = JSON.parse(backupContent); + + if (!backupContent) { + alert(t('无法下载备份文件。', 'Failed to download backup file.')); + return; + } + + { + const backupData = JSON.parse(backupContent);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackupPanel.tsx` around lines 111 - 113, The restore flow currently continues silently when webdavService.downloadFile(latestBackup) returns null/empty (backupContent), causing the spinner to stop with no feedback; update the restore handler in BackupPanel (the code referencing webdavService.downloadFile and backupContent) to explicitly check for falsy/empty backupContent, stop the spinner (e.g., setIsRestoring(false)), surface a clear error to the user (use the component's existing notification/error state handler such as setRestoreError or showNotification), and return early so no further parsing/restore attempts occur.
🧹 Nitpick comments (3)
src/components/ReadmeModal.tsx (1)
78-85: Consider resolving relative image URLs instead of always degrading to placeholders.A large share of README images use relative paths; current behavior causes frequent missing-image UX. Resolving against repo base would preserve content fidelity.
💡 Suggested direction
-interface MarkdownImageProps { +interface MarkdownImageProps { src?: string; alt?: string; + baseUrl?: string; } -const MarkdownImage: React.FC<MarkdownImageProps> = ({ src, alt }) => { +const MarkdownImage: React.FC<MarkdownImageProps> = ({ src, alt, baseUrl }) => { const [hasError, setHasError] = useState(false); if (!src) return null; - const imageUrl = src; - if (src.startsWith('./') || src.startsWith('../')) { - return ( - <span className="text-gray-500 italic"> - [图片: {alt || 'image'}] - </span> - ); - } + const imageUrl = + (src.startsWith('./') || src.startsWith('../') || src.startsWith('/')) && baseUrl + ? new URL(src, `${baseUrl}/raw/HEAD/`).href + : src;- img: MarkdownImage, + img: (props) => <MarkdownImage {...props} baseUrl={repository?.html_url} />,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReadmeModal.tsx` around lines 78 - 85, The current ReadmeModal.tsx branch that returns a placeholder for relative image src (the block checking src.startsWith('./') || src.startsWith('../')) should instead resolve those relative paths against the repository base URL; update the ReadmeModal component to compute a resolved URL (e.g., add or use a prop like repoBaseUrl or implement a helper resolveImageSrc) and replace the placeholder branch so it constructs a full absolute URL via the URL constructor or path join (handling ./, ../ and leading slashes), then render the image normally; also ensure you fallback to the placeholder only if resolution fails or the resolved URL is invalid.src/components/RepositoryCard.tsx (1)
438-470: Consider consolidating theselectionModedisabling pattern.Multiple buttons use the same pattern:
disabled={... || selectionMode}plusclassName={... ${selectionMode ? 'pointer-events-none' : ''}}. This repetition could be extracted into a helper or wrapper component for maintainability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryCard.tsx` around lines 438 - 470, Multiple buttons in RepositoryCard repeat the same selectionMode disable logic; extract a small reusable component (e.g., SelectionAwareButton) or helper that centralizes disabled and className behavior and use it for the buttons invoking handleAIAnalyze, toggleReleaseSubscription, and setEditModalOpen. The helper should accept props like onClick, disabled (base), title, children and internally compute disabled={disabled || selectionMode} and append ${selectionMode ? 'pointer-events-none' : ''} to className so callers (the Bot, Bell/BellOff, Edit3 buttons) no longer repeat the pattern; update RepositoryCard to replace the three repeated button blocks with the new SelectionAwareButton (or wrapper) and pass through isLoading, selectionMode, and existing aria/title props.src/components/AssetFilterManager.tsx (1)
94-108: Consider adding error handling tohandleResetPresets.The reset function calls
deleteAssetFilterandaddAssetFilterin loops synchronously. If any operation fails mid-way, the user could end up with a partial state (some presets deleted, others not added).💡 Optional: wrap in try-catch or use batch update
const handleResetPresets = () => { if (confirm(...)) { + try { presetFilters.forEach(filter => { deleteAssetFilter(filter.id); if (selectedFilters.includes(filter.id)) { onFilterToggle(filter.id); } }); DEFAULT_PRESET_FILTERS.forEach(filter => { addAssetFilter(filter); }); + } catch (error) { + console.error('Failed to reset presets:', error); + // Optionally show user feedback + } } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/AssetFilterManager.tsx` around lines 94 - 108, handleResetPresets currently performs deleteAssetFilter and addAssetFilter in loops without error handling; wrap the reset in a try-catch and perform the operations transactionally (e.g., collect deletions/additions and run them as a batch or via Promise.all, or apply tentative changes and roll back on failure) so partial state is avoided. Specifically, inside handleResetPresets catch errors from deleteAssetFilter/addAssetFilter, restore any deleted filters (or revert UI selection via onFilterToggle) if an add fails, and surface a user-friendly error message; reference the presetFilters loop, deleteAssetFilter, addAssetFilter, onFilterToggle, and DEFAULT_PRESET_FILTERS when implementing the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/BulkActionToolbar.tsx`:
- Around line 48-57: The triggerShake function uses shakeTimeoutRef to reset
isShaking after 500ms but doesn't clear the timeout on unmount; add a useEffect
cleanup that returns a function which clears shakeTimeoutRef.current (if set) to
prevent the timeout firing after unmount. Locate triggerShake, shakeTimeoutRef,
and setIsShaking in BulkActionToolbar and implement the effect cleanup to
clearTimeout(shakeTimeoutRef.current) and null out the ref.
In `@src/components/BulkCategorizeModal.tsx`:
- Around line 32-44: The handleCategorize function currently swallows rejections
from onCategorize; add a catch block to handle failures, set a visible error
state (e.g., via a new setError or setCategorizeError state) and ensure
setIsProcessing(false) runs in finally; when an error occurs display the error
to the user (toast/inline message) and do not call onClose on failure so the
modal remains open for retry; update references in handleCategorize to use the
new error state and ensure onCategorize failures are logged or surfaced.
In `@src/components/ReadmeModal.tsx`:
- Around line 37-39: The component ReadmeModal.tsx currently forces links (via
the anchor rendering that sets target="_blank") even for intra-document hash
links; update the link rendering logic (the branch that checks
link.startsWith('#') and the anchor creation around where target and rel are set
— also the similar block at the other occurrence noted) so that if
link.startsWith('#') you return/render the anchor without target="_blank" (and
without unnecessary rel attributes), otherwise keep the external-link behavior
(target="_blank" and rel="noopener noreferrer"). Ensure both places (the
existing if (link.startsWith('#')) return link; and the later anchor creation at
lines referenced in the review) follow this conditional.
- Around line 118-140: fetchReadme can suffer from race conditions where an
earlier async response overwrites newer state; fix it by adding a
request-sequencing guard: create a stable ref (e.g., readmeRequestIdRef via
useRef(0)), increment it before each fetch, capture the current id in the
fetchReadme closure, and before calling setReadmeContent/setError/setLoading
only apply results when the captured id === readmeRequestIdRef.current;
alternatively use an AbortController and ignore results when aborted. Apply the
same pattern to the other similar fetch block (the other README fetch/update
code that also calls setReadmeContent/setError/setLoading).
In `@src/components/ReleaseTimeline.tsx`:
- Around line 467-473: The cutPoint calculation can become -1 when one of
lastOpenBracket or lastOpenParen is missing; in the block that handles
openBrackets/openParens (using beforeCut, maxLength), replace cutPoint =
Math.min(lastOpenBracket, lastOpenParen) with logic that ignores -1 indices:
compute the valid indices (e.g., filter [lastOpenBracket, lastOpenParen] for >=
0) and set cutPoint to the minimum of those valid indices if any exist,
otherwise leave cutPoint unchanged (or fall back to a safe value like
maxLength/2); update the code referencing openBrackets, openParens, beforeCut,
lastOpenBracket, lastOpenParen, and cutPoint accordingly.
- Around line 793-812: Remove the custom onKeyDown handler on the button in
ReleaseTimeline (the button that calls toggleReleaseExpansion(release.id) and
uses isExpanded/release.id for aria attributes); the native <button> already
handles Enter/Space activation so delete the onKeyDown prop block that checks
e.key === 'Enter' || ' ' and calls toggleReleaseExpansion to avoid
double-toggling, leaving the existing onClick (and any e.stopPropagation) and
aria attributes intact.
In `@src/components/RepositoryList.tsx`:
- Around line 509-582: Initialize analysisProgress before the bulk loop and
increment it as each repo finishes: call setAnalysisProgress({ current: 0,
total: repos.length }) right after setLoading(true) / isAnalyzingRef.current =
true, and inside the per-repo async handler (the async function in batch.map)
increment progress after each repository is processed (success or failure) via
setAnalysisProgress(prev => ({ ...prev, current: prev.current + 1 })). Keep the
existing finally teardown (isAnalyzingRef, shouldStopRef, setLoading,
setAnalysisProgress({ current: 0, total: 0 })) so the UI resets correctly and
ensure the progress update still runs when shouldStopRef.current short-circuits.
In `@src/components/settings/BackendPanel.tsx`:
- Around line 33-45: Wrap calls to backend.checkHealth(), backend.init(), and
backend.verifyAuth() in try/catch blocks so rejected promises don't leave the UI
stuck in 'checking'. For the checkBackend() function and the other code paths
that call backend.init() and backend.verifyAuth(), catch errors, call
setStatus('disconnected') and setHealth(null) (and optionally set an error
state/message), and log or surface the error via existing UI hooks; ensure you
reference the existing functions/variables (checkBackend, backend.checkHealth,
backend.init, backend.verifyAuth, setStatus, setHealth) when adding the error
handling so every await is protected and the panel transitions to 'disconnected'
on failure.
- Around line 111-122: The current guards skip updating local state when the
backend returns an empty array, leaving stale data; instead always apply the
backend snapshot by removing the length checks and call
setRepositories(repoData.repositories), setReleases(releaseData.releases),
setAIConfigs(aiConfigData), and setWebDAVConfigs(webdavConfigData)
unconditionally (or only guard against null/undefined), so an intentional empty
backend snapshot replaces local data.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 176-191: When restoring AI config entries, the code currently
preserves the local isActive flag for updates and forces new configs to
inactive; change both branches to apply the backed-up active state from the
backup object (cfg). Specifically, in the updateAIConfig call replace isActive:
existing.isActive with isActive: cfg.isActive so the restored active state is
applied, and in the addAIConfig call set isActive: cfg.isActive instead of
false; keep the existing masked apiKey handling (apiKey: isMasked ?
existing.apiKey : cfg.apiKey for updates and apiKey: isMasked ? '' : cfg.apiKey
for adds).
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 105-130: The handlers (handleMoveCategory, handleMoveToTop,
handleMoveToBottom and the other reorder path around lines 175-184) rebuild
persisted order from allVisibleCategories which omits hiddenDefaultCategoryIds;
instead, compute the new order for visible IDs only, then merge those reordered
visible IDs back into the existing categoryOrder array (preserving any IDs not
present in allVisibleCategories) before calling setCategoryOrder, e.g., read the
current categoryOrder, replace only the positions of visible ids with the new
visible sequence, and keep hidden ids in their original relative positions.
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 199-208: The deleteCategorySettings handler currently only removes
customCategories and hiddenDefaultCategoryIds but misses resetting new
sidebar-related state; update deleteCategorySettings to also reset categoryOrder
(e.g., to an empty array), collapsedSidebarCategoryCount (e.g., to 0) and
isSidebarCollapsed (e.g., to false) when calling useAppStore.setState so the
full category display configuration is cleared; keep the existing loop that
calls store.deleteCustomCategory(cat.id) and then include these additional keys
in the setState call to fully clear sidebar/category display settings.
In `@src/components/settings/GeneralPanel.tsx`:
- Around line 80-82: Replace the hardcoded "v0.3.0" in GeneralPanel.tsx by
importing the version from package.json and using that variable in the i18n call
instead of the literal; specifically, add an import for version from
package.json and change the JSX line that calls t(...) (the paragraph element
using t('当前版本: v0.3.0', 'Current Version: v0.3.0')) to inject the version value
(e.g. formatted as `v${version}`) into the translated string or pass it as a
placeholder argument to t; ensure your bundler allows JSON imports or use a
generated constant if needed.
In `@src/components/SettingsPanel.tsx`:
- Line 349: The aria-label in SettingsPanel uses t('settings.close', 'Close
settings') which is inconsistent with other literal Chinese defaults (e.g.,
t('设置','Settings');) and may be an unresolved placeholder; update the call in
the SettingsPanel component to use the same translation pattern as other labels
by either replacing 'settings.close' with the matching Chinese key/string (for
example t('关闭','Close settings') or t('设置.关闭','关闭')) or add a proper i18n entry
for 'settings.close' in your locale files, then ensure the aria-label prop calls
t(...) with the correct key/default to show the expected Chinese text.
In `@src/constants/presetFilters.ts`:
- Around line 13-17: Preset keyword overlap is causing false-positive matches;
update the preset objects (e.g., id: 'preset-windows', 'preset-macos',
'preset-linux', 'preset-android', 'preset-source') to remove generic archive
tokens ('.zip', 'tar.gz', 'tar.xz', 'zip') from OS/platform presets and keep
archive extensions only in 'preset-source', and/or replace them in OS presets
with platform-specific installers (e.g., keep '.exe', '.msi' for preset-windows,
'.dmg', '.pkg' for preset-macos, 'appimage', 'deb', 'rpm' for preset-linux) so
source archives no longer match Windows/Linux/Android presets.
---
Outside diff comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 183-208: The delete and hide flows (handleDeleteCategory and
handleHideDefaultCategory) call deleteCustomCategory/hideDefaultCategory then
await forceSyncToBackend() without handling rejection; wrap the await in
try/catch and on failure revert the local change (e.g., restore the
removed/hidden category using the same rollback approach used by the drag-drop
path) and surface the error to the user; specifically, capture previous category
state before calling deleteCustomCategory or hideDefaultCategory, call the
mutation, then await forceSyncToBackend() inside try/catch and in the catch
block restore the previous state (or re-fetch categories) and show an error
notification so UI and backend remain consistent.
In `@src/store/useAppStore.ts`:
- Around line 105-130: The AppState type is missing the new sidebar fields
referenced by PersistedAppState (categoryOrder, collapsedSidebarCategoryCount,
isSidebarCollapsed), causing a type drift; update the AppState definition to
include these three properties with appropriate types (e.g., categoryOrder:
string[] or the existing category id type, collapsedSidebarCategoryCount:
number, isSidebarCollapsed: boolean) so that Pick<AppState, ...> and
PersistedAppState align; ensure any related usages (e.g., PersistedAppState and
useAppStore) compile against the updated AppState shape.
---
Duplicate comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 298-308: The category buttons in CategorySidebar.tsx are only
visually indicating selection; add semantic state so screen readers can announce
the active category by adding aria-pressed={isSelected} (or
aria-current={isSelected && category.id === 'all' ? 'page' : undefined} as
appropriate) to each category <button> that uses onCategorySelect and isSelected
(apply the same change in the three button locations: mobile, collapsed-desktop,
expanded-desktop where the button uses onCategorySelect, category.id,
category.name and t) so assistive tech receives the active-state information.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 155-171: The matchesActiveFilters function is incorrectly using
the static PRESET_FILTERS constant so edited preset keywords are ignored; update
the logic to treat persisted filters in assetFilters as the single source of
truth — compute activeCustomFilters and activePresetFilters by filtering
assetFilters (e.g., by a preset flag or by id membership in selectedFilters)
instead of referencing PRESET_FILTERS, then run the same keyword matching
against those filtered assetFilters within matchesActiveFilters.
- Around line 846-880: The file-toggle button currently uses aria-pressed and
aria-label only; change it to a proper disclosure by replacing aria-pressed with
aria-expanded (bound to openDropdowns.has(release.id)) and adding aria-controls
that points to a stable id for the files panel (e.g.
`files-panel-${release.id}`), and ensure the controlled panel element (the
collapsible file list rendered elsewhere) has that same id and role="region" (or
appropriate landmark) so screen readers can associate the button with the panel;
update references in the expand/collapse logic (setOpenDropdowns,
toggleReleaseExpansion, openDropdowns, and the element that renders the file
panel) to use the stable id but do not change the existing expansion semantics.
- Around line 1009-1026: Add accessible wiring between the toggle button and the
collapsible region: on the button (used in toggleReleaseNotes(release.id, e))
add aria-controls and aria-expanded (e.g.
aria-controls={`release-notes-${release.id}`} and
aria-expanded={!isReleaseNotesCollapsed}) and ensure the collapsible div has a
matching id (id={`release-notes-${release.id}`}); keep the existing visual
behavior using isReleaseNotesCollapsed for the state and update aria-expanded to
reflect open/closed accordingly so assistive tech can determine the section
state.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 111-113: The restore flow currently continues silently when
webdavService.downloadFile(latestBackup) returns null/empty (backupContent),
causing the spinner to stop with no feedback; update the restore handler in
BackupPanel (the code referencing webdavService.downloadFile and backupContent)
to explicitly check for falsy/empty backupContent, stop the spinner (e.g.,
setIsRestoring(false)), surface a clear error to the user (use the component's
existing notification/error state handler such as setRestoreError or
showNotification), and return early so no further parsing/restore attempts
occur.
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 95-98: The APP_LOCALSTORAGE_PREFIXES array currently includes the
broad prefix 'zustand', which risks wiping other apps' persisted state; change
APP_LOCALSTORAGE_PREFIXES to only contain explicit app-owned prefixes (e.g. keep
'github-stars-manager' and remove 'zustand' or replace it with a narrow,
app-specific key like 'zustand-github-stars-manager' or a whitelist of exact
keys), and ensure any code that clears storage (using APP_LOCALSTORAGE_PREFIXES)
only matches those explicit prefixes or exact keys to avoid deleting unrelated
origins' data.
In `@src/components/settings/WebDAVPanel.tsx`:
- Around line 61-72: When saving while editing (editingId set), the code
unconditionally sets isActive: false on the new config and passes it to
updateWebDAVConfig; instead, find the existing WebDAVConfig for that editingId
(e.g., via webdavConfigs.find or a getter used in this component), read its
current isActive value, and reuse that value when building the config object
before calling updateWebDAVConfig(editingId, config); keep the current behavior
of setting isActive: false only when creating a new config (no editingId).
---
Nitpick comments:
In `@src/components/AssetFilterManager.tsx`:
- Around line 94-108: handleResetPresets currently performs deleteAssetFilter
and addAssetFilter in loops without error handling; wrap the reset in a
try-catch and perform the operations transactionally (e.g., collect
deletions/additions and run them as a batch or via Promise.all, or apply
tentative changes and roll back on failure) so partial state is avoided.
Specifically, inside handleResetPresets catch errors from
deleteAssetFilter/addAssetFilter, restore any deleted filters (or revert UI
selection via onFilterToggle) if an add fails, and surface a user-friendly error
message; reference the presetFilters loop, deleteAssetFilter, addAssetFilter,
onFilterToggle, and DEFAULT_PRESET_FILTERS when implementing the fix.
In `@src/components/ReadmeModal.tsx`:
- Around line 78-85: The current ReadmeModal.tsx branch that returns a
placeholder for relative image src (the block checking src.startsWith('./') ||
src.startsWith('../')) should instead resolve those relative paths against the
repository base URL; update the ReadmeModal component to compute a resolved URL
(e.g., add or use a prop like repoBaseUrl or implement a helper resolveImageSrc)
and replace the placeholder branch so it constructs a full absolute URL via the
URL constructor or path join (handling ./, ../ and leading slashes), then render
the image normally; also ensure you fallback to the placeholder only if
resolution fails or the resolved URL is invalid.
In `@src/components/RepositoryCard.tsx`:
- Around line 438-470: Multiple buttons in RepositoryCard repeat the same
selectionMode disable logic; extract a small reusable component (e.g.,
SelectionAwareButton) or helper that centralizes disabled and className behavior
and use it for the buttons invoking handleAIAnalyze, toggleReleaseSubscription,
and setEditModalOpen. The helper should accept props like onClick, disabled
(base), title, children and internally compute disabled={disabled ||
selectionMode} and append ${selectionMode ? 'pointer-events-none' : ''} to
className so callers (the Bot, Bell/BellOff, Edit3 buttons) no longer repeat the
pattern; update RepositoryCard to replace the three repeated button blocks with
the new SelectionAwareButton (or wrapper) and pass through isLoading,
selectionMode, and existing aria/title props.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3d634d20-8fc2-4d3f-997a-e47383137429
⛔ Files ignored due to path filters (2)
dist/index.htmlis excluded by!**/dist/**package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (26)
package.jsonsrc/App.tsxsrc/components/AssetFilterManager.tsxsrc/components/BackToTop.tsxsrc/components/BulkActionToolbar.tsxsrc/components/BulkCategorizeModal.tsxsrc/components/CategorySidebar.tsxsrc/components/ReadmeModal.tsxsrc/components/ReleaseTimeline.tsxsrc/components/RepositoryCard.tsxsrc/components/RepositoryList.tsxsrc/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/GeneralPanel.tsxsrc/components/settings/WebDAVPanel.tsxsrc/components/settings/index.tssrc/constants/presetFilters.tssrc/index.csssrc/services/githubApi.tssrc/store/useAppStore.tssrc/types/index.tstailwind.config.js
更新构建工作流中的 Node.js 版本以使用最新的 LTS 版本
refactor: 重构代码结构提升可维护性 fix: 修复分类排序和恢复备份的问题 perf: 优化动画性能减少卡顿 style: 统一按钮样式和交互体验 docs: 更新版本号显示逻辑 test: 增强错误处理和回滚机制 build: 添加 remark-breaks 依赖
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/components/CategorySidebar.tsx (3)
202-218:⚠️ Potential issue | 🔴 Critical
showDefaultCategoryis undefined in the rollback path.The catch block calls
showDefaultCategory(category.id), but this function is never read fromuseAppStore(). In TS this fails compilation, and even in JS the rollback path would throw instead of restoring the hidden category.🐛 Minimal fix
const { customCategories, hiddenDefaultCategoryIds, categoryOrder, collapsedSidebarCategoryCount, deleteCustomCategory, hideDefaultCategory, + showDefaultCategory, language, updateRepository, isSidebarCollapsed, setSidebarCollapsed, } = useAppStore();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 202 - 218, The catch block in handleHideDefaultCategory calls showDefaultCategory(category.id) but showDefaultCategory is never obtained from useAppStore(), causing a runtime/compile error; fix by reading showDefaultCategory (and hideDefaultCategory if not already) from useAppStore() at the top of the component so both functions are defined, then use those store methods for the optimistic hide and rollback around forceSyncToBackend(); ensure the symbols showDefaultCategory, hideDefaultCategory, useAppStore, handleHideDefaultCategory, and forceSyncToBackend are the ones referenced/updated.
250-255:⚠️ Potential issue | 🟠 MajorDon't persist the localized display name as the category key.
getAllCategories(...)localizes built-in category names, and this path storescategory.nameintocustom_category. After a language switch, that stored value no longer matches the rebuilt category list, so manual categorization can disappear or be counted under the wrong category. Persist a stable id or canonical unlocalized value instead. This same issue also applies to the bulk categorize flow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 250 - 255, The code is persisting the localized display name into custom_category (see nextRepo construction in CategorySidebar.tsx), which breaks after language switches because getAllCategories(...) returns localized names; change the stored value to a stable identifier instead (e.g., category.id or a canonical/unlocalized key like category.key/canonical_name) and update any other paths that set custom_category (including the bulk categorize flow) to persist and read that stable id so category lookups use the canonical value rather than the localized display name.
183-199:⚠️ Potential issue | 🟠 MajorFailed deletes are not rolled back.
deleteCustomCategory(category.id)happens beforeforceSyncToBackend(), but the catch only shows an alert. If backend sync fails, the category stays deleted locally even though the remote write was rejected, so frontend and backend drift immediately.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/CategorySidebar.tsx` around lines 183 - 199, handleDeleteCategory currently calls deleteCustomCategory(category.id) before awaiting forceSyncToBackend, but on sync failure it only alerts and does not restore state; save a backup of the category object first, then either (A) perform the remote deletion first (call forceSyncToBackend with an intent or API delete) and only call deleteCustomCategory(category.id) after a successful backend response, or (B) keep the optimistic delete but wrap deleteCustomCategory(category.id) with try/await so that if forceSyncToBackend throws you immediately call addCustomCategory(backup) (or a restoreCategory method) to revert local state; reference handleDeleteCategory, deleteCustomCategory, addCustomCategory/restoreCategory, and forceSyncToBackend when implementing the rollback/ordering change and ensure UI state is consistent while the operation is in flight.
♻️ Duplicate comments (7)
src/components/ReleaseTimeline.tsx (1)
65-82:⚠️ Potential issue | 🟠 MajorUse the persisted preset filters as the source of truth.
matchesActiveFiltersstill pulls preset keywords fromPRESET_FILTERS, while user-edited preset filters live inassetFilters. That means changes made inAssetFilterManagerstill won't affect matching, counts, or highlighting here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseTimeline.tsx` around lines 65 - 82, matchesActiveFilters currently reads preset keywords from PRESET_FILTERS instead of the persisted preset entries in assetFilters, so update it to derive both activeCustomFilters and activePresetFilters from assetFilters (e.g., activeCustomFilters = assetFilters.filter(f => !f.isPreset && selectedFilters.includes(f.id)) and activePresetFilters = assetFilters.filter(f => f.isPreset && selectedFilters.includes(f.id))) and then run the same keyword matching logic against those sets; keep the useCallback signature and dependency array (selectedFilters, assetFilters) and reference the existing function name matchesActiveFilters and the AssetFilterManager-managed assetFilters structure.src/components/ReleaseCard.tsx (1)
252-269:⚠️ Potential issue | 🟡 MinorExpose the release-notes expanded state semantically.
The "Release Notes" toggle button is missing
aria-expandedandaria-controls, and the content container has no matching id. Screen readers still can't tell whether that section is open.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ReleaseCard.tsx` around lines 252 - 269, The Release Notes toggle button lacks accessibility attributes; update the button in ReleaseCard (the onClick that calls onToggleReleaseNotes and reads isReleaseNotesCollapsed) to include aria-expanded set to the expanded state (e.g., !isReleaseNotesCollapsed) and aria-controls pointing to a unique id, and add a matching id on the content container div rendered when !isReleaseNotesCollapsed (the div below the button) so screen readers can associate the button with the collapsible panel.src/components/SettingsPanel.tsx (1)
332-397:⚠️ Potential issue | 🟠 MajorComplete the modal's keyboard interaction model.
The dialog now has ARIA metadata, but focus is still not moved into it, trapped there, restored on close, or closable via Escape. Keyboard users can still tab behind the overlay and lose context.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SettingsPanel.tsx` around lines 332 - 397, The modal lacks keyboard focus management and Escape-to-close behavior; update the SettingsPanel component to (1) on open save document.activeElement and move focus into the dialog (e.g., to the first focusable in the dialog or the close button), (2) trap focus inside the dialog while open (implement a simple focus loop using tabbable selectors or use a focus-trap helper) so Tab/Shift+Tab never move focus behind the overlay, (3) listen for Escape and call handleClose to close the modal, and (4) on close restore focus to the previously focused element. Add these effects in a useEffect tied to the modal-open state and reference the dialog container (the outer div with role="dialog"), the close button (handleClose), and renderTabContent/activeTab as needed to find the initial focus target.src/components/settings/BackupPanel.tsx (1)
181-198:⚠️ Potential issue | 🟠 MajorRestore still does not apply the backed-up active config state.
For existing configs you keep
existing.isActive, and new WebDAV configs are forced tofalse. A restore can therefore finish with a different active AI/WebDAV profile than the backup describes. The restore path should applycfg.isActiveconsistently, and ifactiveAIConfig/activeWebDAVConfigare the real selectors, make sure those ids are restored too.Also applies to: 221-235
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/BackupPanel.tsx` around lines 181 - 198, The restore logic incorrectly preserves existing.isActive and forces new WebDAV configs to false, so the restored active state doesn't match the backup; modify both AI and WebDAV restore branches (the calls to updateAIConfig/addAIConfig and the analogous updateWebDAVConfig/addWebDAVConfig blocks) to always apply cfg.isActive when restoring (use cfg.isActive for the isActive field instead of existing.isActive or hardcoding false) and ensure the app-level active selectors (activeAIConfig and activeWebDAVConfig) are updated to the restored ids so the same profiles become active after restore.src/components/settings/CategoryPanel.tsx (1)
109-115:⚠️ Potential issue | 🟠 MajorHidden ids still lose their saved position during reorder.
This still rewrites the persisted order as
visibleIds + hiddenIds. Hidden ids are no longer dropped, but they are always pushed to the tail, so unhiding a default category still loses its previous slot. Merge the reordered visible ids back into the existingcategoryOrderarray instead of concatenating them.Also applies to: 123-125, 133-135, 185-190
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/CategoryPanel.tsx` around lines 109 - 115, The current logic builds the new categoryOrder as visibleIds + hiddenIds which moves all hidden IDs to the end; instead merge the reordered visibleIds back into the existing categoryOrder to preserve hidden IDs' original slots. Replace the concatenation step in the handlers that use visibleIds, categoryOrder and setCategoryOrder (the blocks around visibleIds.splice(...), hiddenIds = ..., and setCategoryOrder([...visibleIds, ...hiddenIds])) with logic that iterates the original categoryOrder and for each id: if it is a visible id, consume and place the next id from the reordered visibleIds sequence, otherwise keep the original hidden id in-place; then call setCategoryOrder with that merged array. Apply the same replacement for the other similar blocks referenced (lines ~123-125, ~133-135, ~185-190).src/components/RepositoryList.tsx (1)
523-557:⚠️ Potential issue | 🟠 MajorUse a functional progress update in the bulk AI loop.
The per-repo callback closes over a stale
analysisProgressvalue. Withconcurrency > 1, multiple completions in the same batch can all write the samecurrent + 1, so the progress bar lags or stops advancing. UsesetAnalysisProgress(prev => ...)here.Suggested fix
- const newCurrent = Math.min(analysisProgress.current + 1, analysisProgress.total); - setAnalysisProgress({ current: newCurrent, total: analysisProgress.total }); + setAnalysisProgress(prev => ({ + ...prev, + current: Math.min(prev.current + 1, prev.total), + }));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/RepositoryList.tsx` around lines 523 - 557, The progress update inside the per-repo callback closes over a stale analysisProgress value, causing concurrent completions to overwrite each other; replace the direct setAnalysisProgress({ current: newCurrent, total: ... }) with a functional updater like setAnalysisProgress(prev => ({ current: Math.min(prev.current + 1, prev.total), total: prev.total })) so each completion increments based on the latest state; update any other places in the batch (including failure/error branches) that currently compute newCurrent from analysisProgress to use the functional form; look for usages of setAnalysisProgress, analysisProgress, and the per-repo async callback (where aiService.analyzeRepository, updateRepository, resolveCategoryAssignment are invoked) to apply the change.src/components/settings/DataManagementPanel.tsx (1)
121-127:⚠️ Potential issue | 🟠 Major
clearAllStorage()still cannot detect IndexedDB deletion failures.This
try/catchonly helps ifindexedDBStorage.removeItem()rejects, butsrc/services/indexedDbStorage.tscurrently swallows IndexedDB errors and resolvesvoid.deleteAllData()can therefore log success and reload even when IndexedDB cleanup failed. Please propagate a failure signal from the storage layer (or return a status) before treating the wipe as successful.#!/bin/bash fd -i 'indexedDbStorage.ts' . -x sed -n '153,169p' {}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/DataManagementPanel.tsx` around lines 121 - 127, clearAllStorage() currently assumes indexedDBStorage.removeItem('github-stars-manager') will reject on failure, but indexedDbStorage.ts swallows errors and resolves; update the storage layer (the removeItem function in src/services/indexedDbStorage.ts) to propagate failures (either by rejecting the Promise on error or returning a boolean status), then change clearAllStorage()/deleteAllData() to check that result and throw/log and abort the reload when the storage layer indicates failure rather than always treating the wipe as successful.
🧹 Nitpick comments (3)
src/components/settings/WebDAVPanel.tsx (2)
259-288: Icon-only buttons rely ontitlefor accessible name.The Test, Edit, and Delete buttons use only
titleattributes, which are not reliably announced by screen readers and unavailable on touch devices. Addaria-labelfor accessibility.♿ Proposed accessibility fix
<button onClick={() => handleTest(config)} disabled={testingId === config.id} className="p-2 rounded-lg bg-blue-100 text-blue-600 ..." title={t('测试连接', 'Test Connection')} + aria-label={t('测试连接', 'Test Connection')} >Apply similar changes to the Edit and Delete buttons.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/WebDAVPanel.tsx` around lines 259 - 288, The Test, Edit and Delete buttons use only title attributes which aren't reliable for screen readers or touch; update the three button elements (the one invoking handleTest(config) that checks testingId === config.id, the one invoking handleEdit(config), and the one calling deleteWebDAVConfig(config.id)) to include aria-label attributes using the same localized strings you pass to t(...) (e.g. t('测试连接', 'Test Connection'), t('编辑','Edit'), t('删除','Delete')) so screen readers and touch users get an accessible name.
138-149: Consider addingnamefield validation.The form validates URL, username, password, and path via
WebDAVService.validateConfig, but thenamefield can be saved empty. If a config name is required for identification in the list, consider adding validation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/WebDAVPanel.tsx` around lines 138 - 149, The form allows saving a WebDAV config with an empty name; update WebDAVPanel to validate the name before submit (same place other validations are enforced via WebDAVService.validateConfig) by checking form.name is non-empty and showing an inline error state/message and preventing submit; adjust the submit handler (in WebDAVPanel's save/submit function) to call the new name check, setForm or a local error state for the name field when empty, and only proceed to call WebDAVService.validateConfig and save when name is present.src/components/settings/GeneralPanel.tsx (1)
31-68: Consider extracting the language option as a reusable component.The language radio button markup is duplicated for Chinese and English options. While functional, extracting a
LanguageOptioncomponent would reduce repetition.♻️ Optional refactor to reduce duplication
+const LanguageOption: React.FC<{ + value: 'zh' | 'en'; + checked: boolean; + onChange: (value: 'zh' | 'en') => void; + label: string; + sublabel: string; +}> = ({ value, checked, onChange, label, sublabel }) => ( + <label className="flex items-center space-x-3 cursor-pointer p-3 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"> + <input + type="radio" + name="language" + value={value} + checked={checked} + onChange={(e) => onChange(e.target.value as 'zh' | 'en')} + className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" + /> + <div> + <span className="text-base font-medium text-gray-900 dark:text-white">{label}</span> + <p className="text-xs text-gray-500 dark:text-gray-400">{sublabel}</p> + </div> + </label> +);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/settings/GeneralPanel.tsx` around lines 31 - 68, The language radio markup is duplicated; extract a reusable LanguageOption component and replace the two repeated label blocks with it. Create a LanguageOption functional component that accepts props like value: 'zh' | 'en', label, sublabel, currentLanguage (or language) and onChange (e.g., setLanguage) and renders the input and accompanying text exactly as in the existing blocks; then use <LanguageOption value="zh" label="中文" sublabel="Simplified Chinese" language={language} onChange={setLanguage} /> and similarly for "en". Ensure the input's checked and onChange behavior uses the passed props so existing behavior of setLanguage and language is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/AssetFilterManager.tsx`:
- Around line 94-149: The rollback uses stale closure values for assetFilters
and selectedFilters; update the catch block to read fresh state from the store
(e.g. call useAppStore.getState() and destructure current assetFilters and
selectedFilters) and use those current arrays in all rollback checks (the
existence checks around deleteAssetFilter/addAssetFilter and the loop that
clears/restores selection) instead of the closure-captured
assetFilters/selectedFilters; keep using addedFilterIds, deleteAssetFilter,
addAssetFilter and onFilterToggle as before but perform existence checks against
the freshly-read currentFilters/currentSelected.
In `@src/components/BulkActionToolbar.tsx`:
- Around line 90-104: The delayed callbacks in handleClose and handleDeselectAll
currently call setIsClosing(false) after invoking the parent callbacks, which
can trigger a state update on an unmounted component; remove the trailing
setIsClosing(false) calls from both timeouts so you only setIsClosing(true) then
call onClose() / onDeselectAll() after the delay. Locate the two functions named
handleClose and handleDeselectAll in BulkActionToolbar and delete the
setIsClosing(false) lines inside their setTimeout callbacks (or alternatively
guard them with a mounted check or clear the timeout on unmount if you prefer),
ensuring no state is reset after the parent hides the toolbar.
In `@src/components/MarkdownRenderer.tsx`:
- Around line 82-87: The image error fallback in MarkdownRenderer/MarkdownImage
currently uses hardcoded Chinese text ("[图片加载失败: ...]"); change it to use the
app i18n instead by either adding a language prop to MarkdownRendererProps and
passing it into MarkdownImage, or by importing and using useAppStore (or the
existing i18n selector) directly inside MarkdownImage; replace the hardcoded
string in the hasError branch with a localized message (e.g., use
t('image.loadFailed') or a language-based ternary) and keep the alt fallback
behavior (alt || 'image') intact so the displayed placeholder is localized while
preserving the alt text.
In `@src/components/RepositoryCard.tsx`:
- Line 445: The card currently disables drag based on presence of the onSelect
callback (draggable={!onSelect}), which breaks CategorySidebar drop flow; change
the gating to use the selection mode flag instead (e.g., use
draggable={!selectionMode} or draggable={selectionMode === 'none'} depending on
how selectionMode is represented) inside the RepositoryCard component so cards
remain draggable when selection support exists but bulk-selection mode is off;
update any related drag/dragStart logic in RepositoryCard to reference
selectionMode rather than onSelect.
In `@src/components/RepositoryList.tsx`:
- Around line 510-513: The bulk analysis path sets isAnalyzingRef.current and
shows the shared pause UI but never honors pause—mirror handleAIAnalyze’s pause
gate inside the repository iteration: inside the bulk analysis loop (the
function that iterates repos and updates setAnalysisProgress /
isAnalyzingRef.current) add a wait loop like while (isPaused &&
!shouldStopRef.current) await new Promise(r => setTimeout(r, 200)); so the Pause
button actually pauses processing; ensure you reference the same pause flag
(isPaused or isPausedRef) and shouldStopRef.current and keep
isAnalyzingRef.current behavior unchanged.
In `@src/components/settings/BackendPanel.tsx`:
- Around line 89-93: The backend sync call is only persisting
hiddenDefaultCategoryIds; update the payloads sent to and received from the
backend so full category/sidebar metadata is round-tripped: include
customCategories, categoryOrder, collapsedSidebarCategoryCount, and
isSidebarCollapsed alongside hiddenDefaultCategoryIds when calling
backend.syncSettings and when handling backend.restore responses (locate the
calls to backend.syncSettings and the restore/restore handlers in
BackendPanel.tsx and add those properties to the settings object and to any
state updates that apply to repositories' custom_category and sidebar
rendering). Ensure the same expanded payload is used in both sync directions so
repositories with custom_category map to actual categories and ordering is
preserved across devices.
- Around line 33-50: The effect in BackendPanel uses backend.checkHealth()
before the backend adapter is initialized, so on fresh load it reports
disconnected; update the useEffect to call and await backend.init() (from
src/services/backendAdapter.ts which discovers _backendUrl) before calling
backend.checkHealth(), handle/init errors by catching and setting status/health
consistently, and ensure you only call setStatus('connected') when checkHealth()
returns valid data after successful init; reference the useEffect in
BackendPanel, backend.init(), backend.checkHealth(), and the adapter's
_backendUrl discovery.
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 49-67: Prevent duplicate category names when adding or renaming by
checking the full category list for an existing name (case-insensitive) before
calling addCustomCategory or updateCustomCategory; in handleAddCategory (and the
edit handler around the same file) compute if any existing category.name equals
the new name excluding the category being edited (use its id when editing) and
show the same validation alert if a duplicate is found. Also ensure
updateCustomCategory and deleteCustomCategory logic in useAppStore.ts relies on
category id (not repo.custom_category name) when matching categories to repos or
update the store callers to pass/compare ids consistently to avoid cascading
changes via repo.custom_category string comparisons.
In `@src/components/SettingsPanel.tsx`:
- Around line 153-155: The mobile tab nav is using
aria-controls="mobile-tabpanel-*" while the tab panels are rendered with a
different id scheme via renderTabContent('desktop'), causing broken references;
update MobileTabNav (and the other occurrences you noted) so aria-controls and
id use a shared scheme (e.g., tabpanel-{tab.id}) or make renderTabContent emit
matching "mobile-tabpanel-{tab.id}" ids when the mobile tablist is active;
specifically, change the id/aria-controls logic in the MobileTabNav component
and the renderTabContent function (and the TabPanel rendering sites referenced
around lines you noted) so both nav variants point to the same panel id format
and aria-controls always matches the actual panel id in the DOM.
In `@src/index.css`:
- Around line 44-57: There are two conflicting animations: the CSS keyframes
named "slide-down" and class ".animate-slide-down" (translating to 100%) vs the
Tailwind keyframe "slideDown" (10px) in tailwind.config.js; resolve by renaming
or consolidating so names and intent are unique—e.g., rename the custom
keyframes and class to "slide-down-full" (update "@keyframes slide-down" ->
"@keyframes slide-down-full" and ".animate-slide-down" ->
".animate-slide-down-full") or alternatively remove the custom definition and
map the Tailwind "slideDown" to the desired full-screen behavior, then update
all usages of ".animate-slide-down" or the Tailwind utility to the chosen name
to avoid load-order conflicts.
In `@src/store/useAppStore.ts`:
- Around line 272-279: The preset keywords in defaultPresetFilters (constant
defaultPresetFilters in useAppStore.ts) are inconsistent with the canonical list
in presetFilters (e.g., removing archive extensions like ".zip" and "tar.gz"
from platform presets); update defaultPresetFilters to match the canonical
keywords by removing archive extensions from platform entries OR replace the
literal array with an import of the canonical preset list and map over it to add
the UI-only fields (isPreset, icon, id/name as needed) just like
AssetFilterManager/DEFAULT_PRESET_FILTERS does so both sources share identical
keyword sets.
---
Outside diff comments:
In `@src/components/CategorySidebar.tsx`:
- Around line 202-218: The catch block in handleHideDefaultCategory calls
showDefaultCategory(category.id) but showDefaultCategory is never obtained from
useAppStore(), causing a runtime/compile error; fix by reading
showDefaultCategory (and hideDefaultCategory if not already) from useAppStore()
at the top of the component so both functions are defined, then use those store
methods for the optimistic hide and rollback around forceSyncToBackend(); ensure
the symbols showDefaultCategory, hideDefaultCategory, useAppStore,
handleHideDefaultCategory, and forceSyncToBackend are the ones
referenced/updated.
- Around line 250-255: The code is persisting the localized display name into
custom_category (see nextRepo construction in CategorySidebar.tsx), which breaks
after language switches because getAllCategories(...) returns localized names;
change the stored value to a stable identifier instead (e.g., category.id or a
canonical/unlocalized key like category.key/canonical_name) and update any other
paths that set custom_category (including the bulk categorize flow) to persist
and read that stable id so category lookups use the canonical value rather than
the localized display name.
- Around line 183-199: handleDeleteCategory currently calls
deleteCustomCategory(category.id) before awaiting forceSyncToBackend, but on
sync failure it only alerts and does not restore state; save a backup of the
category object first, then either (A) perform the remote deletion first (call
forceSyncToBackend with an intent or API delete) and only call
deleteCustomCategory(category.id) after a successful backend response, or (B)
keep the optimistic delete but wrap deleteCustomCategory(category.id) with
try/await so that if forceSyncToBackend throws you immediately call
addCustomCategory(backup) (or a restoreCategory method) to revert local state;
reference handleDeleteCategory, deleteCustomCategory,
addCustomCategory/restoreCategory, and forceSyncToBackend when implementing the
rollback/ordering change and ensure UI state is consistent while the operation
is in flight.
---
Duplicate comments:
In `@src/components/ReleaseCard.tsx`:
- Around line 252-269: The Release Notes toggle button lacks accessibility
attributes; update the button in ReleaseCard (the onClick that calls
onToggleReleaseNotes and reads isReleaseNotesCollapsed) to include aria-expanded
set to the expanded state (e.g., !isReleaseNotesCollapsed) and aria-controls
pointing to a unique id, and add a matching id on the content container div
rendered when !isReleaseNotesCollapsed (the div below the button) so screen
readers can associate the button with the collapsible panel.
In `@src/components/ReleaseTimeline.tsx`:
- Around line 65-82: matchesActiveFilters currently reads preset keywords from
PRESET_FILTERS instead of the persisted preset entries in assetFilters, so
update it to derive both activeCustomFilters and activePresetFilters from
assetFilters (e.g., activeCustomFilters = assetFilters.filter(f => !f.isPreset
&& selectedFilters.includes(f.id)) and activePresetFilters =
assetFilters.filter(f => f.isPreset && selectedFilters.includes(f.id))) and then
run the same keyword matching logic against those sets; keep the useCallback
signature and dependency array (selectedFilters, assetFilters) and reference the
existing function name matchesActiveFilters and the AssetFilterManager-managed
assetFilters structure.
In `@src/components/RepositoryList.tsx`:
- Around line 523-557: The progress update inside the per-repo callback closes
over a stale analysisProgress value, causing concurrent completions to overwrite
each other; replace the direct setAnalysisProgress({ current: newCurrent, total:
... }) with a functional updater like setAnalysisProgress(prev => ({ current:
Math.min(prev.current + 1, prev.total), total: prev.total })) so each completion
increments based on the latest state; update any other places in the batch
(including failure/error branches) that currently compute newCurrent from
analysisProgress to use the functional form; look for usages of
setAnalysisProgress, analysisProgress, and the per-repo async callback (where
aiService.analyzeRepository, updateRepository, resolveCategoryAssignment are
invoked) to apply the change.
In `@src/components/settings/BackupPanel.tsx`:
- Around line 181-198: The restore logic incorrectly preserves existing.isActive
and forces new WebDAV configs to false, so the restored active state doesn't
match the backup; modify both AI and WebDAV restore branches (the calls to
updateAIConfig/addAIConfig and the analogous updateWebDAVConfig/addWebDAVConfig
blocks) to always apply cfg.isActive when restoring (use cfg.isActive for the
isActive field instead of existing.isActive or hardcoding false) and ensure the
app-level active selectors (activeAIConfig and activeWebDAVConfig) are updated
to the restored ids so the same profiles become active after restore.
In `@src/components/settings/CategoryPanel.tsx`:
- Around line 109-115: The current logic builds the new categoryOrder as
visibleIds + hiddenIds which moves all hidden IDs to the end; instead merge the
reordered visibleIds back into the existing categoryOrder to preserve hidden
IDs' original slots. Replace the concatenation step in the handlers that use
visibleIds, categoryOrder and setCategoryOrder (the blocks around
visibleIds.splice(...), hiddenIds = ..., and setCategoryOrder([...visibleIds,
...hiddenIds])) with logic that iterates the original categoryOrder and for each
id: if it is a visible id, consume and place the next id from the reordered
visibleIds sequence, otherwise keep the original hidden id in-place; then call
setCategoryOrder with that merged array. Apply the same replacement for the
other similar blocks referenced (lines ~123-125, ~133-135, ~185-190).
In `@src/components/settings/DataManagementPanel.tsx`:
- Around line 121-127: clearAllStorage() currently assumes
indexedDBStorage.removeItem('github-stars-manager') will reject on failure, but
indexedDbStorage.ts swallows errors and resolves; update the storage layer (the
removeItem function in src/services/indexedDbStorage.ts) to propagate failures
(either by rejecting the Promise on error or returning a boolean status), then
change clearAllStorage()/deleteAllData() to check that result and throw/log and
abort the reload when the storage layer indicates failure rather than always
treating the wipe as successful.
In `@src/components/SettingsPanel.tsx`:
- Around line 332-397: The modal lacks keyboard focus management and
Escape-to-close behavior; update the SettingsPanel component to (1) on open save
document.activeElement and move focus into the dialog (e.g., to the first
focusable in the dialog or the close button), (2) trap focus inside the dialog
while open (implement a simple focus loop using tabbable selectors or use a
focus-trap helper) so Tab/Shift+Tab never move focus behind the overlay, (3)
listen for Escape and call handleClose to close the modal, and (4) on close
restore focus to the previously focused element. Add these effects in a
useEffect tied to the modal-open state and reference the dialog container (the
outer div with role="dialog"), the close button (handleClose), and
renderTabContent/activeTab as needed to find the initial focus target.
---
Nitpick comments:
In `@src/components/settings/GeneralPanel.tsx`:
- Around line 31-68: The language radio markup is duplicated; extract a reusable
LanguageOption component and replace the two repeated label blocks with it.
Create a LanguageOption functional component that accepts props like value: 'zh'
| 'en', label, sublabel, currentLanguage (or language) and onChange (e.g.,
setLanguage) and renders the input and accompanying text exactly as in the
existing blocks; then use <LanguageOption value="zh" label="中文"
sublabel="Simplified Chinese" language={language} onChange={setLanguage} /> and
similarly for "en". Ensure the input's checked and onChange behavior uses the
passed props so existing behavior of setLanguage and language is preserved.
In `@src/components/settings/WebDAVPanel.tsx`:
- Around line 259-288: The Test, Edit and Delete buttons use only title
attributes which aren't reliable for screen readers or touch; update the three
button elements (the one invoking handleTest(config) that checks testingId ===
config.id, the one invoking handleEdit(config), and the one calling
deleteWebDAVConfig(config.id)) to include aria-label attributes using the same
localized strings you pass to t(...) (e.g. t('测试连接', 'Test Connection'),
t('编辑','Edit'), t('删除','Delete')) so screen readers and touch users get an
accessible name.
- Around line 138-149: The form allows saving a WebDAV config with an empty
name; update WebDAVPanel to validate the name before submit (same place other
validations are enforced via WebDAVService.validateConfig) by checking form.name
is non-empty and showing an inline error state/message and preventing submit;
adjust the submit handler (in WebDAVPanel's save/submit function) to call the
new name check, setForm or a local error state for the name field when empty,
and only proceed to call WebDAVService.validateConfig and save when name is
present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 952e3718-741b-4f4e-927f-fed27e09f1ae
⛔ Files ignored due to path filters (2)
dist/index.htmlis excluded by!**/dist/**package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (29)
.github/workflows/build-desktop.ymlpackage.jsonsrc/App.tsxsrc/components/AssetFilterManager.tsxsrc/components/BackToTop.tsxsrc/components/BulkActionToolbar.tsxsrc/components/BulkCategorizeModal.tsxsrc/components/CategorySidebar.tsxsrc/components/MarkdownRenderer.tsxsrc/components/ReadmeModal.tsxsrc/components/ReleaseCard.tsxsrc/components/ReleaseTimeline.tsxsrc/components/RepositoryCard.tsxsrc/components/RepositoryList.tsxsrc/components/SettingsPanel.tsxsrc/components/settings/AIConfigPanel.tsxsrc/components/settings/BackendPanel.tsxsrc/components/settings/BackupPanel.tsxsrc/components/settings/CategoryPanel.tsxsrc/components/settings/DataManagementPanel.tsxsrc/components/settings/GeneralPanel.tsxsrc/components/settings/WebDAVPanel.tsxsrc/components/settings/index.tssrc/constants/presetFilters.tssrc/index.csssrc/services/githubApi.tssrc/store/useAppStore.tssrc/types/index.tstailwind.config.js
| const handleResetPresets = () => { | ||
| if (confirm(language === 'zh' ? '确定要重置所有预设筛选器吗?这将恢复默认设置。' : 'Are you sure you want to reset all preset filters? This will restore default settings.')) { | ||
| // 保存完整状态快照以便回滚(使用深拷贝确保状态独立) | ||
| const previousFilters = assetFilters.map(f => ({ ...f })); | ||
| const previousSelected = [...selectedFilters]; | ||
| const addedFilterIds: string[] = []; | ||
|
|
||
| try { | ||
| // 删除所有现有的预设筛选器 | ||
| presetFilters.forEach(filter => { | ||
| if (assetFilters.find(f => f.id === filter.id)) { | ||
| deleteAssetFilter(filter.id); | ||
| } | ||
| if (selectedFilters.includes(filter.id)) { | ||
| onFilterToggle(filter.id); | ||
| } | ||
| }); | ||
| // 添加默认预设筛选器 | ||
| DEFAULT_PRESET_FILTERS.forEach(filter => { | ||
| if (!assetFilters.find(f => f.id === filter.id)) { | ||
| addAssetFilter(filter); | ||
| addedFilterIds.push(filter.id); | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| // 回滚到之前的状态 | ||
| console.error('Failed to reset presets:', error); | ||
|
|
||
| // 1. 移除新添加的筛选器 | ||
| addedFilterIds.forEach(id => { | ||
| if (assetFilters.find(f => f.id === id)) { | ||
| deleteAssetFilter(id); | ||
| } | ||
| }); | ||
|
|
||
| // 2. 恢复之前的筛选器(包括被删除的) | ||
| previousFilters.forEach(filter => { | ||
| if (!assetFilters.find(f => f.id === filter.id)) { | ||
| addAssetFilter(filter); | ||
| } | ||
| }); | ||
|
|
||
| // 3. 完全恢复之前的选择状态(先清除所有,再恢复) | ||
| // 清除当前所有选择 | ||
| [...selectedFilters].forEach(id => { | ||
| onFilterToggle(id); | ||
| }); | ||
| // 恢复之前的选择 | ||
| previousSelected.forEach(id => { | ||
| onFilterToggle(id); | ||
| }); | ||
|
|
||
| alert(language === 'zh' ? '重置预设筛选器失败,已恢复之前的状态。' : 'Failed to reset preset filters. Previous state has been restored.'); | ||
| } | ||
| } | ||
| }; |
There was a problem hiding this comment.
Rollback logic references stale closure state.
The catch block uses assetFilters and selectedFilters to perform rollback, but these are captured at function entry. Since deleteAssetFilter and addAssetFilter are synchronous Zustand updates, if an exception occurs mid-operation, the closure's assetFilters won't reflect the partial mutations. The rollback checks (Lines 104, 113, 124, 131) will use outdated data.
In practice, Zustand store operations rarely throw, so this is low-risk. However, if robustness is desired, consider reading fresh state via useAppStore.getState().assetFilters in the catch block.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/AssetFilterManager.tsx` around lines 94 - 149, The rollback
uses stale closure values for assetFilters and selectedFilters; update the catch
block to read fresh state from the store (e.g. call useAppStore.getState() and
destructure current assetFilters and selectedFilters) and use those current
arrays in all rollback checks (the existence checks around
deleteAssetFilter/addAssetFilter and the loop that clears/restores selection)
instead of the closure-captured assetFilters/selectedFilters; keep using
addedFilterIds, deleteAssetFilter, addAssetFilter and onFilterToggle as before
but perform existence checks against the freshly-read
currentFilters/currentSelected.
| const handleClose = () => { | ||
| setIsClosing(true); | ||
| setTimeout(() => { | ||
| onClose(); | ||
| setIsClosing(false); | ||
| }, 300); | ||
| }; | ||
|
|
||
| const handleDeselectAll = () => { | ||
| setIsClosing(true); | ||
| setTimeout(() => { | ||
| onDeselectAll(); | ||
| setIsClosing(false); | ||
| }, 300); | ||
| }; |
There was a problem hiding this comment.
Avoid resetting local state after the parent closes the toolbar.
Both delayed callbacks invoke onClose() / onDeselectAll() and then immediately call setIsClosing(false). In the normal RepositoryList flow those callbacks hide the toolbar, so the trailing state update is unnecessary and can race with unmount.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/BulkActionToolbar.tsx` around lines 90 - 104, The delayed
callbacks in handleClose and handleDeselectAll currently call
setIsClosing(false) after invoking the parent callbacks, which can trigger a
state update on an unmounted component; remove the trailing setIsClosing(false)
calls from both timeouts so you only setIsClosing(true) then call onClose() /
onDeselectAll() after the delay. Locate the two functions named handleClose and
handleDeselectAll in BulkActionToolbar and delete the setIsClosing(false) lines
inside their setTimeout callbacks (or alternatively guard them with a mounted
check or clear the timeout on unmount if you prefer), ensuring no state is reset
after the parent hides the toolbar.
| if (hasError) { | ||
| return ( | ||
| <span className="text-gray-500 italic"> | ||
| [图片加载失败: {alt || 'image'}] | ||
| </span> | ||
| ); |
There was a problem hiding this comment.
Hardcoded Chinese text in image error fallback.
The error placeholder [图片加载失败: {alt || 'image'}] is hardcoded in Chinese, but the app supports both Chinese and English locales. This is inconsistent with the i18n approach used elsewhere.
🌐 Suggested fix to add i18n support
Since MarkdownRenderer doesn't receive the language prop, you could either:
- Add a
languageprop toMarkdownRendererPropsand pass it through, or - Access
useAppStoredirectly inMarkdownImage:
+import { useAppStore } from '../store/useAppStore';
+
const MarkdownImage: React.FC<{ src?: string; alt?: string; baseUrl?: string }> = ({
src,
alt,
baseUrl
}) => {
const [hasError, setHasError] = React.useState(false);
+ const language = useAppStore(state => state.language);
// ...
if (hasError) {
return (
<span className="text-gray-500 italic">
- [图片加载失败: {alt || 'image'}]
+ {language === 'zh'
+ ? `[图片加载失败: ${alt || 'image'}]`
+ : `[Image failed to load: ${alt || 'image'}]`}
</span>
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (hasError) { | |
| return ( | |
| <span className="text-gray-500 italic"> | |
| [图片加载失败: {alt || 'image'}] | |
| </span> | |
| ); | |
| import { useAppStore } from '../store/useAppStore'; | |
| const MarkdownImage: React.FC<{ src?: string; alt?: string; baseUrl?: string }> = ({ | |
| src, | |
| alt, | |
| baseUrl | |
| }) => { | |
| const [hasError, setHasError] = React.useState(false); | |
| const language = useAppStore(state => state.language); | |
| // ... rest of component code ... | |
| if (hasError) { | |
| return ( | |
| <span className="text-gray-500 italic"> | |
| {language === 'zh' | |
| ? `[图片加载失败: ${alt || 'image'}]` | |
| : `[Image failed to load: ${alt || 'image'}]`} | |
| </span> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/MarkdownRenderer.tsx` around lines 82 - 87, The image error
fallback in MarkdownRenderer/MarkdownImage currently uses hardcoded Chinese text
("[图片加载失败: ...]"); change it to use the app i18n instead by either adding a
language prop to MarkdownRendererProps and passing it into MarkdownImage, or by
importing and using useAppStore (or the existing i18n selector) directly inside
MarkdownImage; replace the hardcoded string in the hasError branch with a
localized message (e.g., use t('image.loadFailed') or a language-based ternary)
and keep the alt fallback behavior (alt || 'image') intact so the displayed
placeholder is localized while preserving the alt text.
| className={`repository-card bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 hover:shadow-lg transition-all duration-200 hover:border-blue-300 dark:hover:border-blue-600 flex flex-col h-full cursor-pointer ${ | ||
| isSelected ? 'shadow-[0_0_0_2px_theme(colors.blue.500)] dark:shadow-[0_0_0_2px_theme(colors.blue.400)] bg-blue-50 dark:bg-blue-900/20' : '' | ||
| } ${isExitingSelection && isSelected ? 'animate-selection-exit' : ''}`} | ||
| draggable={!onSelect} |
There was a problem hiding this comment.
Don't disable dragging just because selection support exists.
Line 445 uses draggable={!onSelect}. In the list flow, onSelect can be wired even when bulk-selection mode is off, so cards stop being draggable altogether and the CategorySidebar drop flow breaks in normal browsing. This should be gated by selectionMode, not by the mere presence of the callback.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/RepositoryCard.tsx` at line 445, The card currently disables
drag based on presence of the onSelect callback (draggable={!onSelect}), which
breaks CategorySidebar drop flow; change the gating to use the selection mode
flag instead (e.g., use draggable={!selectionMode} or draggable={selectionMode
=== 'none'} depending on how selectionMode is represented) inside the
RepositoryCard component so cards remain draggable when selection support exists
but bulk-selection mode is off; update any related drag/dragStart logic in
RepositoryCard to reference selectionMode rather than onSelect.
| // 设置加载状态 | ||
| setLoading(true); | ||
| isAnalyzingRef.current = true; | ||
| setAnalysisProgress({ current: 0, total: repos.length }); |
There was a problem hiding this comment.
Pause is exposed for bulk AI, but this branch never actually pauses.
This path enables the shared loading/progress UI, so users get a Pause button, but unlike handleAIAnalyze() the bulk loop never waits on isPaused. Clicking Pause only flips the button state while analysis keeps running. Either mirror the while (isPaused && !shouldStopRef.current) gate here or hide pause for bulk analysis.
Also applies to: 523-573
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/RepositoryList.tsx` around lines 510 - 513, The bulk analysis
path sets isAnalyzingRef.current and shows the shared pause UI but never honors
pause—mirror handleAIAnalyze’s pause gate inside the repository iteration:
inside the bulk analysis loop (the function that iterates repos and updates
setAnalysisProgress / isAnalyzingRef.current) add a wait loop like while
(isPaused && !shouldStopRef.current) await new Promise(r => setTimeout(r, 200));
so the Pause button actually pauses processing; ensure you reference the same
pause flag (isPaused or isPausedRef) and shouldStopRef.current and keep
isAnalyzingRef.current behavior unchanged.
| await backend.syncRepositories(repositories); | ||
| await backend.syncReleases(releases); | ||
| await backend.syncAIConfigs(aiConfigs); | ||
| await backend.syncWebDAVConfigs(webdavConfigs); | ||
| await backend.syncSettings({ hiddenDefaultCategoryIds }); |
There was a problem hiding this comment.
Backend sync is not round-tripping the new category metadata.
Only hiddenDefaultCategoryIds is persisted here. customCategories, categoryOrder, collapsedSidebarCategoryCount, and isSidebarCollapsed never leave the client, so a backend restore can bring back repositories whose custom_category values have no matching category definitions or ordering on the target device. Please include the full category/sidebar settings payload in both sync directions.
Also applies to: 119-145
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/settings/BackendPanel.tsx` around lines 89 - 93, The backend
sync call is only persisting hiddenDefaultCategoryIds; update the payloads sent
to and received from the backend so full category/sidebar metadata is
round-tripped: include customCategories, categoryOrder,
collapsedSidebarCategoryCount, and isSidebarCollapsed alongside
hiddenDefaultCategoryIds when calling backend.syncSettings and when handling
backend.restore responses (locate the calls to backend.syncSettings and the
restore/restore handlers in BackendPanel.tsx and add those properties to the
settings object and to any state updates that apply to repositories'
custom_category and sidebar rendering). Ensure the same expanded payload is used
in both sync directions so repositories with custom_category map to actual
categories and ordering is preserved across devices.
| const handleAddCategory = () => { | ||
| if (!newCategoryName.trim()) { | ||
| alert(t('请输入分类名称', 'Please enter category name')); | ||
| return; | ||
| } | ||
|
|
||
| const newCategory = { | ||
| id: `custom-${Date.now()}`, | ||
| name: newCategoryName.trim(), | ||
| icon: newCategoryIcon, | ||
| isCustom: true, | ||
| keywords: [], | ||
| }; | ||
|
|
||
| addCustomCategory(newCategory); | ||
| setNewCategoryName(''); | ||
| setNewCategoryIcon('📁'); | ||
| setShowAddForm(false); | ||
| }; |
There was a problem hiding this comment.
Reject duplicate category names before add/edit.
updateCustomCategory() and deleteCustomCategory() in src/store/useAppStore.ts cascade by comparing repo.custom_category to the category name, not the id. With the current validation, adding or renaming a category to an existing name can make one edit/delete affect repositories that belong to a different category. Please enforce name uniqueness across the full category list (excluding the category currently being edited).
Also applies to: 75-90
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/settings/CategoryPanel.tsx` around lines 49 - 67, Prevent
duplicate category names when adding or renaming by checking the full category
list for an existing name (case-insensitive) before calling addCustomCategory or
updateCustomCategory; in handleAddCategory (and the edit handler around the same
file) compute if any existing category.name equals the new name excluding the
category being edited (use its id when editing) and show the same validation
alert if a duplicate is found. Also ensure updateCustomCategory and
deleteCustomCategory logic in useAppStore.ts relies on category id (not
repo.custom_category name) when matching categories to repos or update the store
callers to pass/compare ids consistently to avoid cascading changes via
repo.custom_category string comparisons.
| id={`mobile-tab-${tab.id}`} | ||
| aria-selected={activeTab === tab.id} | ||
| aria-controls={`mobile-tabpanel-${tab.id}`} |
There was a problem hiding this comment.
Use one tabpanel id scheme for both nav variants.
MobileTabNav exposes aria-controls="mobile-tabpanel-*" here, but both render paths still mount renderTabContent('desktop'), so on mobile the visible tabs point to a panel that never exists. Either make the panel ids shared across layouts or render the matching mobile-* panel ids when the mobile tablist is active.
Also applies to: 313-316, 392-392, 450-450
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/SettingsPanel.tsx` around lines 153 - 155, The mobile tab nav
is using aria-controls="mobile-tabpanel-*" while the tab panels are rendered
with a different id scheme via renderTabContent('desktop'), causing broken
references; update MobileTabNav (and the other occurrences you noted) so
aria-controls and id use a shared scheme (e.g., tabpanel-{tab.id}) or make
renderTabContent emit matching "mobile-tabpanel-{tab.id}" ids when the mobile
tablist is active; specifically, change the id/aria-controls logic in the
MobileTabNav component and the renderTabContent function (and the TabPanel
rendering sites referenced around lines you noted) so both nav variants point to
the same panel id format and aria-controls always matches the actual panel id in
the DOM.
| @keyframes slide-down { | ||
| from { | ||
| transform: translateY(0); | ||
| opacity: 1; | ||
| } | ||
| to { | ||
| transform: translateY(100%); | ||
| opacity: 0; | ||
| } | ||
| } | ||
|
|
||
| .animate-slide-down { | ||
| animation: slide-down 0.3s ease-in forwards; | ||
| } |
There was a problem hiding this comment.
Duplicate slide-down animation with different behavior.
This slide-down keyframe translates to 100% (fully off-screen), but tailwind.config.js defines slideDown translating to 10px (subtle movement). Both generate .animate-slide-down classes, potentially causing conflicts or unexpected behavior depending on CSS load order.
Consider renaming one or consolidating to a single definition.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/index.css` around lines 44 - 57, There are two conflicting animations:
the CSS keyframes named "slide-down" and class ".animate-slide-down"
(translating to 100%) vs the Tailwind keyframe "slideDown" (10px) in
tailwind.config.js; resolve by renaming or consolidating so names and intent are
unique—e.g., rename the custom keyframes and class to "slide-down-full" (update
"@keyframes slide-down" -> "@keyframes slide-down-full" and
".animate-slide-down" -> ".animate-slide-down-full") or alternatively remove the
custom definition and map the Tailwind "slideDown" to the desired full-screen
behavior, then update all usages of ".animate-slide-down" or the Tailwind
utility to the chosen name to avoid load-order conflicts.
| // 默认预设筛选器 | ||
| const defaultPresetFilters: AssetFilter[] = [ | ||
| { id: 'preset-windows', name: 'Windows', keywords: ['windows', 'win', 'exe', 'msi', '.zip'], isPreset: true, icon: 'Monitor' }, | ||
| { id: 'preset-macos', name: 'macOS', keywords: ['mac', 'macos', 'darwin', 'dmg', 'pkg'], isPreset: true, icon: 'Apple' }, | ||
| { id: 'preset-linux', name: 'Linux', keywords: ['linux', 'appimage', 'deb', 'rpm', 'tar.gz'], isPreset: true, icon: 'Terminal' }, | ||
| { id: 'preset-android', name: 'Android', keywords: ['android', 'apk'], isPreset: true, icon: 'Smartphone' }, | ||
| { id: 'preset-source', name: 'Source', keywords: ['source', 'src', 'tar.gz', 'tar.xz', 'zip'], isPreset: true, icon: 'Package' }, | ||
| ]; |
There was a problem hiding this comment.
Inconsistent preset filter keywords between files.
The defaultPresetFilters here includes .zip for Windows (Line 274) and tar.gz for Linux (Line 276), but src/constants/presetFilters.ts has already removed these archive extensions from platform presets to avoid false-positive matches. This inconsistency means users may get different filter behavior depending on code path.
🐛 Proposed fix to align with presetFilters.ts
const defaultPresetFilters: AssetFilter[] = [
- { id: 'preset-windows', name: 'Windows', keywords: ['windows', 'win', 'exe', 'msi', '.zip'], isPreset: true, icon: 'Monitor' },
+ { id: 'preset-windows', name: 'Windows', keywords: ['windows', 'win', 'exe', 'msi'], isPreset: true, icon: 'Monitor' },
{ id: 'preset-macos', name: 'macOS', keywords: ['mac', 'macos', 'darwin', 'dmg', 'pkg'], isPreset: true, icon: 'Apple' },
- { id: 'preset-linux', name: 'Linux', keywords: ['linux', 'appimage', 'deb', 'rpm', 'tar.gz'], isPreset: true, icon: 'Terminal' },
+ { id: 'preset-linux', name: 'Linux', keywords: ['linux', 'appimage', 'deb', 'rpm'], isPreset: true, icon: 'Terminal' },
{ id: 'preset-android', name: 'Android', keywords: ['android', 'apk'], isPreset: true, icon: 'Smartphone' },
{ id: 'preset-source', name: 'Source', keywords: ['source', 'src', 'tar.gz', 'tar.xz', 'zip'], isPreset: true, icon: 'Package' },
];Alternatively, consider importing from src/constants/presetFilters.ts and mapping to add isPreset and icon fields, similar to how AssetFilterManager.tsx does it with DEFAULT_PRESET_FILTERS.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/store/useAppStore.ts` around lines 272 - 279, The preset keywords in
defaultPresetFilters (constant defaultPresetFilters in useAppStore.ts) are
inconsistent with the canonical list in presetFilters (e.g., removing archive
extensions like ".zip" and "tar.gz" from platform presets); update
defaultPresetFilters to match the canonical keywords by removing archive
extensions from platform entries OR replace the literal array with an import of
the canonical preset list and map over it to add the UI-only fields (isPreset,
icon, id/name as needed) just like AssetFilterManager/DEFAULT_PRESET_FILTERS
does so both sources share identical keyword sets.



为订阅和查看GitHub按钮添加中文标题支持,根据当前语言环境显示相应文本
Summary by CodeRabbit
New Features
Refactor
Style
Bug Fixes