Skip to content

feat: 多语言按钮支持/重构设置界面/新增数据管理/优化侧边栏/新增返回顶部按钮#81

Open
SummerRay160 wants to merge 19 commits intoAmintaCCCP:mainfrom
SummerRay160:main
Open

feat: 多语言按钮支持/重构设置界面/新增数据管理/优化侧边栏/新增返回顶部按钮#81
SummerRay160 wants to merge 19 commits intoAmintaCCCP:mainfrom
SummerRay160:main

Conversation

@SummerRay160
Copy link
Copy Markdown

@SummerRay160 SummerRay160 commented Apr 14, 2026

为订阅和查看GitHub按钮添加中文标题支持,根据当前语言环境显示相应文本

Summary by CodeRabbit

  • New Features

    • Tabbed Settings with dedicated panels (General, AI, WebDAV, Backup, Backend, Category, Data Management) and mobile tab nav
    • Repository multi-select, bulk actions (toolbar, categorize, AI summary, subscribe/unsubscribe, unstar) and bulk-categorize modal
    • Readme modal + Markdown renderer with safer links/images
    • Back-to-top button, WebDAV backup/restore, backend sync, preset asset filters, category management and ordering
  • Refactor

    • Settings UI split into focused, tab-driven subpanels; release list refactored into cards
  • Style

    • New utilities (scrollbar-hide, touch-manipulation) and animations (slide, fade, shake, bounce)
  • Bug Fixes

    • README decoding now preserves UTF-8

为订阅和查看GitHub按钮添加中文标题支持,根据当前语言环境显示相应文本
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Settings shell & subpanels
src/components/SettingsPanel.tsx, src/components/settings/...
src/components/settings/AIConfigPanel.tsx, src/components/settings/WebDAVPanel.tsx, src/components/settings/BackupPanel.tsx, src/components/settings/BackendPanel.tsx, src/components/settings/CategoryPanel.tsx, src/components/settings/DataManagementPanel.tsx, src/components/settings/GeneralPanel.tsx
Replaces monolithic settings with a tabbed shell and delegated subpanels. Adds props, transition state, mobile tab nav, modal mode, and discrete panels for AI/WebDAV/Backup/Backend/Category/DataManagement/General logic and UI.
Settings barrel export
src/components/settings/index.ts
Adds barrel re-exports for the new settings subcomponents.
Repository list, cards & bulk flows
src/components/RepositoryList.tsx, src/components/RepositoryCard.tsx
Adds multi-selection state and props (isSelected, onSelect, selectionMode, isExitingSelection), selection-aware button wrapper, README modal wiring, click/keyboard selection handling, and bulk action integration (toolbar/modal + handlers).
Bulk UI components
src/components/BulkActionToolbar.tsx, src/components/BulkCategorizeModal.tsx
New bottom bulk-action toolbar with confirm/processing semantics and a modal to bulk-assign categories.
Readme & Markdown rendering
src/components/ReadmeModal.tsx, src/components/MarkdownRenderer.tsx, src/services/githubApi.ts
Adds Readme modal (fetch + cancellation, focus management), a MarkdownRenderer using react-markdown with safe HTML option and custom renderers, and changes README base64 decoding to UTF‑8.
Release UI refactor
src/components/ReleaseTimeline.tsx, src/components/ReleaseCard.tsx
Refactors releases into ReleaseCard, changes truncation logic, pagination default, download matching, and interaction controls (dropdown/expand/collapse) with memoized helpers.
Backup / Backend / WebDAV flows
src/components/settings/BackupPanel.tsx, src/components/settings/BackendPanel.tsx, src/components/settings/WebDAVPanel.tsx
Implements backup/restore via WebDAV (masked secrets & restore-preserve rules), backend health/auth and sync-to/from logic with apply/merge rules, and WebDAV config CRUD/test/validation flows.
Category sidebar, store & types
src/components/CategorySidebar.tsx, src/store/useAppStore.ts, src/types/index.ts
Adds responsive/collapsible sidebar with delayed label reveal, Ctrl/Cmd+B shortcut, drag/reorder and revert-on-sync-failure, persisted categoryOrder, collapsedSidebarCategoryCount, isSidebarCollapsed; store actions and migration updated; types extended for new state and AssetFilter fields.
Asset filters & presets
src/components/AssetFilterManager.tsx, src/constants/presetFilters.ts
Introduces preset filters constant, icon mapping, preset vs custom separation, reset-presets flow, and expandable preset/custom UI.
Data management & app utilities
src/components/DataManagementPanel.tsx, src/components/BackToTop.tsx, src/App.tsx, src/index.css, tailwind.config.js
Adds DataManagementPanel (deletion flows, purge all), BackToTop component wired into App, new CSS utilities/animations and repository-card attribute rules, and new Tailwind keyframes/animations.
AssetFilterManager tweaks
src/components/AssetFilterManager.tsx
Adds preset support, normalized filters, expanded panel UI, reset/rollback handling, and header controls (clear all, new filter label change).
Deps & CI
package.json, .github/workflows/build-desktop.yml
Adds runtime deps for markdown rendering (react-markdown, rehype-raw, rehype-sanitize, remark-gfm), adjusts devDeps and updates desktop workflow Node → 22.
Other refinements
src/components/CategorySidebar.tsx, src/components/ReleaseTimeline.tsx, src/components/RepositoryList.tsx, src/components/AssetFilterManager.tsx
Multiple UX/interaction refinements: outside-click handling, keyboard shortcuts, accessibility attributes, drag/drop index handling, animation-managed mount/unmount, and localized messages.

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

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through tabs and toggles bright,
backups tucked in WebDAV's gentle light,
Readmes bloom in markdown, links take flight,
sidebars shrink and shortcuts make things right,
我啃着代码,开心地抱着一根小胡萝卜 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is a Chinese feature list (multilingual buttons/settings refactor/data management/sidebar optimization/back-to-top button) that accurately reflects major changeset topics but uses a verbose slash-separated format that is harder to scan and prioritize than a single focused statement.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

新增设置模块的多个面板组件,包括通用设置、WebDAV配置、备份恢复、分类管理、后端服务和AI配置。每个面板提供相应的功能界面和交互逻辑,支持多语言切换。

- 通用设置面板支持语言切换和版本检查
- WebDAV面板提供配置管理和测试功能
- 备份面板支持数据备份和恢复操作
- 分类面板管理自定义分类和默认分类显示
- 后端面板处理服务器连接和数据同步
- AI面板配置AI服务参数和测试连接
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.0 is 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 with Date.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 in handleStartEdit parameter.

The function parameter uses { id: string; name: string; icon: string } but based on the context snippet, the category objects from getAllCategories have a label field instead of name. This may cause category.name to be undefined when editing default categories (though this function is only called for custom categories which do have name).

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' for reasoningEffort.

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:

  1. The filename format changes
  2. 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.json which:

  • 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-auto which 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 handleSyncToBackend throws (e.g., syncReleases fails after syncRepositories succeeds), the user gets an error alert but the backend is left in a partially synced state. Consider either:

  1. Adding per-operation error handling with rollback
  2. Documenting this behavior to users
  3. 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:

  1. Adding a comment explaining this is intentional protection
  2. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 67a62c1 and d439362.

⛔ Files ignored due to path filters (1)
  • dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (8)
  • src/components/SettingsPanel.tsx
  • src/components/settings/AIConfigPanel.tsx
  • src/components/settings/BackendPanel.tsx
  • src/components/settings/BackupPanel.tsx
  • src/components/settings/CategoryPanel.tsx
  • src/components/settings/GeneralPanel.tsx
  • src/components/settings/WebDAVPanel.tsx
  • src/components/settings/index.ts

Comment thread src/components/settings/AIConfigPanel.tsx
Comment thread src/components/settings/BackendPanel.tsx Outdated
Comment thread src/components/settings/BackupPanel.tsx Outdated
Comment thread src/components/settings/BackupPanel.tsx Outdated
Comment thread src/components/settings/CategoryPanel.tsx
Comment thread src/components/settings/WebDAVPanel.tsx
Comment thread src/components/settings/WebDAVPanel.tsx
添加数据管理面板组件,支持查看数据统计和选择性删除不同类型的数据
包括仓库数据、发布信息、AI配置、WebDAV配置和分类设置
提供危险区域可删除所有数据,需GitHub用户名验证
@SummerRay160 SummerRay160 changed the title feat(RepositoryCard): 添加多语言支持按钮标题 feat(RepositoryCard): 添加多语言支持按钮标题/重构设置界面/增加数据管理功能 Apr 14, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/components/settings/DataManagementPanel.tsx (2)

79-87: Toast timeouts may fire after component unmount.

The setTimeout calls in showSuccess and showError aren't cleaned up on unmount, which could cause state updates on an unmounted component.

Consider using useEffect cleanup 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 forEach loop deletes categories one by one, but if deleteCustomCategory triggers any async side effects, they won't be awaited. Additionally, iterating over store.customCategories while mutating it via deleteCustomCategory could 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

📥 Commits

Reviewing files that changed from the base of the PR and between d439362 and a125de6.

⛔ Files ignored due to path filters (1)
  • dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (3)
  • src/components/SettingsPanel.tsx
  • src/components/settings/DataManagementPanel.tsx
  • src/components/settings/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/settings/index.ts

Comment thread src/components/settings/DataManagementPanel.tsx
Comment thread src/components/settings/DataManagementPanel.tsx
Comment thread src/components/SettingsPanel.tsx Outdated
@SummerRay160
Copy link
Copy Markdown
Author

image

- 新增侧边栏折叠状态管理,支持快捷键(Ctrl+B)切换
- 为移动端和桌面端分别优化侧边栏显示方式
- 添加返回顶部按钮组件,提升长页面浏览体验
- 更新相关状态管理以支持新功能
@SummerRay160
Copy link
Copy Markdown
Author

image image

@SummerRay160
Copy link
Copy Markdown
Author

@coderabbitai

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

Tip

For best results, initiate chat on the files or code changes.

@SummerRay160 It looks like you tagged me but didn't include a specific question or request! 😊 Could you let me know what you'd like help with? For example:

  • Would you like me to review the PR?
  • Do you have a specific question about the code or the screenshots you shared?
  • Is there a particular issue you'd like me to look into?

Feel free to share more details and I'll be happy to help!

@SummerRay160
Copy link
Copy Markdown
Author

SummerRay160 commented Apr 14, 2026

@coderabbitai review the PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

@SummerRay160 Sure! I'll kick off a review of the PR right away. 🚀

✅ 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a125de6 and 033ce97.

⛔ Files ignored due to path filters (1)
  • dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (5)
  • src/App.tsx
  • src/components/BackToTop.tsx
  • src/components/CategorySidebar.tsx
  • src/store/useAppStore.ts
  • src/types/index.ts
✅ Files skipped from review due to trivial changes (1)
  • src/types/index.ts

Comment thread src/components/BackToTop.tsx Outdated
@SummerRay160
Copy link
Copy Markdown
Author

@coderabbitai generate docstrings

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

✅ Actions performed

Initiated docstring generation; will generate only if new commits exist.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

Caution

Docstrings generation - FAILED

An unexpected error occurred while opening a pull request: Cannot open a stacked PR on a forked repository.

@SummerRay160 SummerRay160 changed the title feat(RepositoryCard): 添加多语言支持按钮标题/重构设置界面/增加数据管理功能 feat(RepositoryCard): 添加多语言支持按钮标题/重构设置界面/增加数据管理功能/优化侧栏/增加后到顶部按钮 Apr 14, 2026
@SummerRay160
Copy link
Copy Markdown
Author

@AmintaCCCP

@SummerRay160
Copy link
Copy Markdown
Author

@coderabbitai review the PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

@SummerRay160 Sure! I'll kick off a review of the PR right away. 🚀

✅ 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/components/settings/WebDAVPanel.tsx (1)

61-78: ⚠️ Potential issue | 🟡 Minor

Bug: 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) when allCategories.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: true is set redundantly.

The store's addCustomCategory method already adds isCustom: true (per src/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 reasoningEffort values, but the AIReasoningEffort type (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

📥 Commits

Reviewing files that changed from the base of the PR and between 033ce97 and 1cb6843.

📒 Files selected for processing (9)
  • src/components/BackToTop.tsx
  • src/components/CategorySidebar.tsx
  • src/components/SettingsPanel.tsx
  • src/components/settings/AIConfigPanel.tsx
  • src/components/settings/BackendPanel.tsx
  • src/components/settings/BackupPanel.tsx
  • src/components/settings/CategoryPanel.tsx
  • src/components/settings/DataManagementPanel.tsx
  • src/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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
src/components/settings/WebDAVPanel.tsx (1)

61-75: ⚠️ Potential issue | 🟠 Major

Preserve isActive when 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

📥 Commits

Reviewing files that changed from the base of the PR and between 033ce97 and 1cb6843.

📒 Files selected for processing (9)
  • src/components/BackToTop.tsx
  • src/components/CategorySidebar.tsx
  • src/components/SettingsPanel.tsx
  • src/components/settings/AIConfigPanel.tsx
  • src/components/settings/BackendPanel.tsx
  • src/components/settings/BackupPanel.tsx
  • src/components/settings/CategoryPanel.tsx
  • src/components/settings/DataManagementPanel.tsx
  • src/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

Comment thread src/components/BackToTop.tsx
Comment thread src/components/BackToTop.tsx Outdated
Comment thread src/components/CategorySidebar.tsx
Comment thread src/components/settings/BackupPanel.tsx
Comment thread src/components/settings/BackupPanel.tsx
Comment thread src/components/settings/DataManagementPanel.tsx
为多个按钮组件添加aria-label属性,确保屏幕阅读器能正确识别按钮功能。同时修复BackToTop组件初始可见状态和DataManagementPanel中存储清理的逻辑
@SummerRay160
Copy link
Copy Markdown
Author

@coderabbitai review the PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

@SummerRay160 Sure! I'll kick off a review of the PR right away. 🚀

✅ 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/components/CategorySidebar.tsx (1)

395-425: ⚠️ Potential issue | 🟡 Minor

Add explicit labels to the icon-only row actions.

The edit/delete/hide buttons still rely on title only. Please add matching aria-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 for customCategories for consistency.

The code correctly uses useAppStore.getState().hiddenDefaultCategoryIds (line 139) to get fresh state, but uses the potentially stale customCategories from 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb6843 and 500b1d6.

📒 Files selected for processing (4)
  • src/components/BackToTop.tsx
  • src/components/CategorySidebar.tsx
  • src/components/settings/BackupPanel.tsx
  • src/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

Comment thread src/components/CategorySidebar.tsx Outdated
Comment thread src/components/CategorySidebar.tsx Outdated
Comment thread src/components/CategorySidebar.tsx Outdated
Comment thread src/components/CategorySidebar.tsx Outdated
Comment thread src/components/settings/BackupPanel.tsx Outdated
添加移动端标签导航组件,支持平滑滚动和触摸滑动切换
引入滚动条隐藏和触摸优化样式类
重构设置面板的标签切换逻辑,添加过渡动画效果
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (2)
src/components/SettingsPanel.tsx (2)

153-155: ⚠️ Potential issue | 🟠 Major

Keep tab IDs unique across mobile and desktop navs.

Both tab navigations stay mounted and reuse the same tab-${tab.id} IDs, so the panel’s aria-labelledby can 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 | 🟡 Minor

Localize 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 return undefined for unknown preset IDs.

If PRESET_FILTERS contains an entry with an id not present in PRESET_ICON_MAP, the resulting filter will have icon: undefined. While the component handles this gracefully with the fallback const 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: handleResetPresets may create duplicate filters if called multiple times.

The function deletes existing presets then adds defaults, but if addAssetFilter doesn't deduplicate by ID, calling reset while some presets are missing could lead to duplicates. Also, the loop calls onFilterToggle for 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 adding aria-describedby for richer context.

The aria-label provides the repository name and description, which is good. For cards with AI analysis status or subscription state, consider adding aria-describedby pointing 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

📥 Commits

Reviewing files that changed from the base of the PR and between db8dce4 and c8d3c34.

⛔ Files ignored due to path filters (1)
  • dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (10)
  • src/components/AssetFilterManager.tsx
  • src/components/BackToTop.tsx
  • src/components/BulkActionToolbar.tsx
  • src/components/CategorySidebar.tsx
  • src/components/ReadmeModal.tsx
  • src/components/ReleaseTimeline.tsx
  • src/components/RepositoryCard.tsx
  • src/components/RepositoryList.tsx
  • src/components/SettingsPanel.tsx
  • src/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

Comment thread src/components/ReleaseTimeline.tsx Outdated
Comment thread src/components/ReleaseTimeline.tsx Outdated
Comment thread src/components/ReleaseTimeline.tsx Outdated
Comment on lines +565 to +601
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();
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +53 to +70
// 使用 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]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +332 to +397
<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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

HappySummer added 3 commits April 15, 2026 21:09
refactor(RepositoryList): 提取选中仓库数组为统一变量
refactor(ReadmeModal): 重构Markdown链接处理逻辑
feat(CategorySidebar): 添加文字显示动画效果
fix(SettingsPanel): 修复移动端标签页指示器位置问题
feat(AssetFilterManager): 增加按钮可访问性属性
feat(ReleaseTimeline): 改进发布说明折叠和文件列表交互
移除SettingsPanel和CategorySidebar组件中的合并冲突标记
@SummerRay160
Copy link
Copy Markdown
Author

@coderabbitai review the PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

@SummerRay160 Sure! I'll kick off a review of the PR right away. 🚀

✅ 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔴 Critical

Update RepositoryCard to accept the new selection props.

RepositoryCard is still declared with only repository, showAISummary, and searchQuery in src/components/RepositoryCard.tsx:12-22. Passing isSelected and onSelect here 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 | 🟠 Major

Resolve relative README links against a blob base, not repo root.

new URL(link, baseUrl + '/') yields URLs like https://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 | 🟠 Major

Make 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 | 🟡 Minor

Don’t treat !className as “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 | 🟠 Major

Expose 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

📥 Commits

Reviewing files that changed from the base of the PR and between c8d3c34 and 2aa1cee.

📒 Files selected for processing (7)
  • src/components/AssetFilterManager.tsx
  • src/components/BulkActionToolbar.tsx
  • src/components/CategorySidebar.tsx
  • src/components/ReadmeModal.tsx
  • src/components/ReleaseTimeline.tsx
  • src/components/RepositoryList.tsx
  • src/components/SettingsPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/BulkActionToolbar.tsx
  • src/components/SettingsPanel.tsx

Comment thread src/components/CategorySidebar.tsx Outdated
Comment thread src/components/ReadmeModal.tsx Outdated
Comment thread src/components/ReadmeModal.tsx Outdated
Comment thread src/components/ReleaseTimeline.tsx
Comment thread src/components/ReleaseTimeline.tsx Outdated
Comment thread src/components/RepositoryList.tsx
Comment thread src/components/RepositoryList.tsx Outdated
fix(AssetFilterManager): 为筛选按钮添加无障碍属性
feat(ReadmeModal): 引入rehype-sanitize插件增强安全性
refactor(ReleaseTimeline): 改进折叠按钮的无障碍支持
perf(RepositoryList): 优化批量AI分析流程
style(RepositoryCard): 调整选择模式下的交互逻辑
refactor(CategorySidebar): 优化分类拖放交互体验
@SummerRay160
Copy link
Copy Markdown
Author

@coderabbitai review the PR and edit the title

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

🧩 Analysis chain

@SummerRay160, I'll kick off a review and also fix the PR title for you. The current title has two issues:

  1. Scope too narrowfeat(RepositoryCard) doesn't reflect the full breadth of changes (settings refactor, sidebar, back-to-top, data management, etc.)
  2. Typo — "增加到顶部按钮" → should be "新增返回顶部按钮"

I'll update it to:

feat: 多语言按钮支持/重构设置界面/新增数据管理/优化侧边栏/新增返回顶部按钮


🏁 Script executed:

#!/bin/bash
gh pr edit 81 \
  --repo AmintaCCCP/GithubStarsManager \
  --title "feat: 多语言按钮支持/重构设置界面/新增数据管理/优化侧边栏/新增返回顶部按钮"

Length of output: 297


@SummerRay160, unfortunately the command to edit the PR title is blocked due to workflow restriction settings on this repository, so I'm unable to update it directly.

You can update it manually! Here's the corrected title to copy-paste:

feat: 多语言按钮支持/重构设置界面/新增数据管理/优化侧边栏/新增返回顶部按钮

Changes made:

  • Removed the overly narrow (RepositoryCard) scope prefix — the PR covers settings, sidebar, store, back-to-top, and more.
  • Fixed the typo: 增加后到顶部按钮新增返回顶部按钮

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

@SummerRay160 SummerRay160 changed the title feat(RepositoryCard): 添加多语言支持按钮标题/重构设置界面/增加数据管理功能/优化侧栏/增加后到顶部按钮 feat: 多语言按钮支持/重构设置界面/新增数据管理/优化侧边栏/新增返回顶部按钮 Apr 15, 2026
@SummerRay160
Copy link
Copy Markdown
Author

@CodeRabbit Summarize this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

Tip

For best results, initiate chat on the files or code changes.

@SummerRay160 Here is a comprehensive summary of PR #81:


PR Summary

🎯 Overview

This 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 Area

1. 🌐 Multilingual Button Support (RepositoryCard.tsx)

  • The title attributes of the "Subscribe to Releases / Unsubscribe" and "View on GitHub" buttons are now language-aware, switching between Chinese and English based on the current language value from the store.
  • Added optional isSelected / onSelect props for bulk selection: when onSelect is provided, the card becomes a selectable container with a checkbox, ring highlight, and keyboard interaction support.
  • Integrates a new ReadmeModal for displaying repository README content inline.

2. ⚙️ Settings Panel Refactor (SettingsPanel.tsx)

  • Replaced the monolithic settings component (~1,327 lines removed) with a tab-driven layout that delegates to imported subpanels.
  • New SettingsPanelProps interface: isOpen, onClose, isModal — supporting both modal dialog and standalone page display modes.
  • Added animated tab transitions and a responsive MobileTabNav with scroll-to-active and bottom indicator behavior.

3. 🧩 New Settings Subcomponents (src/components/settings/)

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 + B keyboard shortcut toggles sidebar state.
  • isSidebarCollapsed state 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_FILTERS for 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: boolean to AppState and store actions.
  • assetFilters now defaults to defaultPresetFilters instead of an empty array.
  • AssetFilter type extended with optional isPreset and icon fields.

⚠️ Notable Issues to Address Before Merging

  1. Unresolved merge conflict markers in src/components/ReadmeModal.tsx and src/components/RepositoryList.tsx — these <<<<<<< HEAD / ======= / >>>>>>> markers will cause compilation failures. These must be resolved before the PR can be safely merged.

  2. BackupPanel stores masked secrets (***) — ensure restore logic correctly preserves existing secrets for masked entries to avoid data loss.

  3. DataManagementPanel calls window.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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Handle 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 | 🟠 Major

Preset filter edits are not respected in filtering logic.

matchesActiveFilters still reads preset keywords from the static PRESET_FILTERS constant. If users edit preset filters via AssetFilterManager, those edits are stored in assetFilters, 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')} and aria-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 deleteAssetFilter and addAssetFilter in 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/catch around updateRepository is ineffective.

The conditional check if (!releaseSubscriptions.has(repo.id)) before toggling correctly prevents unsubscribing already-subscribed repos, addressing the previous concern.

However, the try/catch around updateRepository (a synchronous zustand action) is dead code—it will never throw. Consider removing it or wrapping only the await 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 filteredDownloadLinks and showFilteredCount.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2aa1cee and e0c3355.

⛔ Files ignored due to path filters (2)
  • dist/index.html is excluded by !**/dist/**
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • package.json
  • src/components/AssetFilterManager.tsx
  • src/components/CategorySidebar.tsx
  • src/components/ReadmeModal.tsx
  • src/components/ReleaseTimeline.tsx
  • src/components/RepositoryCard.tsx
  • src/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

Comment thread src/components/CategorySidebar.tsx
Comment thread src/components/CategorySidebar.tsx
Comment thread src/components/ReleaseTimeline.tsx Outdated
Comment thread src/components/ReleaseTimeline.tsx Outdated
修复 Markdown 代码块渲染逻辑,正确处理内联代码和代码块。优化侧边栏组件,使用 useRef 管理定时器并添加清理逻辑防止内存泄漏。同时改进仓库拖拽分类的错误处理,失败时恢复原始状态并提示用户。
@SummerRay160
Copy link
Copy Markdown
Author

@CodeRabbit Summarize and review PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

@SummerRay160 Here is a summary of PR #81, and I'm triggering a full review as well! 🚀


PR #81 Summary

🎯 Overview

A large feature update (~+2,300 net lines, ~8 new files, ~7 modified files) covering multilingual UI support, a full settings panel refactor, bulk selection/data management, sidebar optimization, and a back-to-top button.


📋 Key Changes

1. 🌐 Multilingual Button Support (RepositoryCard.tsx)

  • title attributes for "Subscribe/Unsubscribe to Releases" and "View on GitHub" are now language-aware (Chinese/English).
  • Added isSelected, onSelect, selectionMode props for bulk selection with checkbox UI and keyboard support.
  • Integrates a new ReadmeModal for inline README rendering.

2. ⚙️ Settings Panel Refactor (SettingsPanel.tsx)

  • Monolithic component (~1,327 lines) replaced with a tab-driven layout delegating to subpanels.
  • New SettingsPanelProps: isOpen, onClose, isModal — supports both modal and standalone page display.
  • Responsive mobile tab nav with scroll-to-active and animated transitions.

3. 🧩 New Settings Subcomponents (src/components/settings/)

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 + B keyboard shortcut to toggle sidebar.
  • isSidebarCollapsed persisted 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_FILTERS for 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

  • isSidebarCollapsed added to AppState and persisted store.
  • AssetFilter extended with optional isPreset and icon.

⚠️ Notable Issues to Address Before Merging

  1. Unresolved merge conflict markerssrc/components/ReadmeModal.tsx and src/components/RepositoryList.tsx likely contain <<<<<<< HEAD / ======= / >>>>>>> markers that will cause compilation failures and must be resolved.
  2. BackupPanel masked secrets (***) — verify that restore logic correctly preserves existing secrets for masked entries to prevent credential data loss.
  3. DataManagementPanel window.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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/components/CategorySidebar.tsx (2)

293-319: ⚠️ Potential issue | 🟡 Minor

Expose 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 appropriate aria-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 | 🟡 Minor

Cancel the pending showText timer before scheduling a new toggle.

A collapse triggered during the first 200ms after expand still leaves showTextTimerRef alive, so the old callback can flip labels back on before the delayed collapse finishes. Clear both timer refs at the start of toggleSidebar().

♻️ 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: Memoize subscribedReleases to avoid unnecessary recalculations.

subscribedReleases creates a new array reference on every render. Since it's used as an input to the filteredReleases useMemo (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 filteredLinks once per release card and reusing the value.

♻️ Suggested approach

Compute filteredLinks at 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 filteredLinks and filteredCount throughout 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

📥 Commits

Reviewing files that changed from the base of the PR and between e0c3355 and 448285c.

📒 Files selected for processing (2)
  • src/components/CategorySidebar.tsx
  • src/components/ReleaseTimeline.tsx

Comment thread src/components/ReleaseTimeline.tsx Outdated
- 在 AppState 中新增 categoryOrder 和 collapsedSidebarCategoryCount 字段
- 实现分类拖拽排序功能,支持上下移动、置顶置底操作
- 优化侧边栏折叠状态下的分类显示逻辑
- 新增退出多选模式时的卡片动画效果
- 改进批量操作工具栏的交互体验
- 更新持久化存储版本至 v2 并添加迁移逻辑
@SummerRay160
Copy link
Copy Markdown
Author

大概就这些了

@SummerRay160
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Full review triggered.

@SummerRay160
Copy link
Copy Markdown
Author

@CodeRabbit Summarize PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

Tip

For best results, initiate chat on the files or code changes.

@SummerRay160 Here's a summary of PR #81:


PR #81 Summary

🎯 Overview

A substantial feature update (~+2,300 net lines, ~8 new files, ~7 modified files) that introduces multilingual UI support, refactors the settings panel into modular subcomponents, adds bulk selection & data management, optimizes the category sidebar, and adds a back-to-top button.


📋 Key Changes

1. 🌐 Multilingual Button Support (RepositoryCard.tsx)

  • title attributes for "Subscribe/Unsubscribe to Releases" and "View on GitHub" are now language-aware (Chinese/English).
  • Added isSelected, onSelect, selectionMode props for bulk selection with checkbox UI and keyboard support.
  • Integrates a new ReadmeModal for inline README rendering.

2. ⚙️ Settings Panel Refactor (SettingsPanel.tsx)

  • Monolithic component (~1,327 lines removed) replaced with a tab-driven layout delegating to subpanels.
  • New SettingsPanelProps: isOpen, onClose, isModal — supports both modal and standalone page display.
  • Responsive mobile tab nav with scroll-to-active and animated tab transitions.

3. 🧩 New Settings Subcomponents (src/components/settings/)

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 + B keyboard shortcut to toggle sidebar.
  • isSidebarCollapsed persisted 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_FILTERS for 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: boolean added to AppState and persisted store.
  • AssetFilter extended with optional isPreset and icon fields.
  • assetFilters now defaults to defaultPresetFilters instead of an empty array.

⚠️ Issues to Address Before Merging

  1. Unresolved merge conflict markerssrc/components/ReadmeModal.tsx and src/components/RepositoryList.tsx may contain <<<<<<< HEAD / ======= / >>>>>>> markers that will cause compilation failures and must be resolved.
  2. BackupPanel masked secrets (***) — verify that restore logic correctly preserves existing credentials for masked entries to prevent data loss.
  3. DataManagementPanel calls window.location.reload() — intentional after "delete all," but confirm this is the expected UX behavior.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔴 Critical

Add the new sidebar fields to AppState as well.

This store now persists and exposes categoryOrder, collapsedSidebarCategoryCount, and isSidebarCollapsed, but the provided src/types/index.ts snippet still omits them from AppState. That makes this Pick<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 | 🟠 Major

Handle 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 | 🟠 Major

Do 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 | 🟡 Minor

Expose the active category state semantically in every layout.

The selected category is still visual-only here. Add aria-pressed={isSelected} or aria-current to 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 | 🟠 Major

Preserve isActive when editing an existing WebDAV config.

The edit path still rebuilds the config with isActive: false and sends it to updateWebDAVConfig, so saving an active entry deactivates it. Look up the existing config first and update only the mutable fields while keeping its current isActive value.

♻️ 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 | 🟠 Major

Use persisted preset filters as the source of truth.

Lines 159-167 still combine assetFilters with the static PRESET_FILTERS constant. 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 | 🟡 Minor

Expose 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 need aria-expanded and aria-controls, plus a stable id on 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 | 🟡 Minor

Wire 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 matching id. 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 | 🟡 Minor

Handle downloadFile() returning no content explicitly.

Lines 111-113 still fall through silently when downloadFile() returns null or 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 the selectionMode disabling pattern.

Multiple buttons use the same pattern: disabled={... || selectionMode} plus className={... ${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 to handleResetPresets.

The reset function calls deleteAssetFilter and addAssetFilter in 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

📥 Commits

Reviewing files that changed from the base of the PR and between b909560 and faf6097.

⛔ Files ignored due to path filters (2)
  • dist/index.html is excluded by !**/dist/**
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (26)
  • package.json
  • src/App.tsx
  • src/components/AssetFilterManager.tsx
  • src/components/BackToTop.tsx
  • src/components/BulkActionToolbar.tsx
  • src/components/BulkCategorizeModal.tsx
  • src/components/CategorySidebar.tsx
  • src/components/ReadmeModal.tsx
  • src/components/ReleaseTimeline.tsx
  • src/components/RepositoryCard.tsx
  • src/components/RepositoryList.tsx
  • src/components/SettingsPanel.tsx
  • src/components/settings/AIConfigPanel.tsx
  • src/components/settings/BackendPanel.tsx
  • src/components/settings/BackupPanel.tsx
  • src/components/settings/CategoryPanel.tsx
  • src/components/settings/DataManagementPanel.tsx
  • src/components/settings/GeneralPanel.tsx
  • src/components/settings/WebDAVPanel.tsx
  • src/components/settings/index.ts
  • src/constants/presetFilters.ts
  • src/index.css
  • src/services/githubApi.ts
  • src/store/useAppStore.ts
  • src/types/index.ts
  • tailwind.config.js

Comment thread src/components/BulkActionToolbar.tsx
Comment thread src/components/BulkCategorizeModal.tsx
Comment thread src/components/ReadmeModal.tsx Outdated
Comment thread src/components/ReadmeModal.tsx
Comment thread src/components/ReleaseTimeline.tsx Outdated
Comment thread src/components/settings/CategoryPanel.tsx Outdated
Comment thread src/components/settings/DataManagementPanel.tsx
Comment thread src/components/settings/GeneralPanel.tsx
Comment thread src/components/SettingsPanel.tsx Outdated
Comment thread src/constants/presetFilters.ts Outdated
HappySummer added 2 commits April 16, 2026 02:00
更新构建工作流中的 Node.js 版本以使用最新的 LTS 版本
refactor: 重构代码结构提升可维护性

fix: 修复分类排序和恢复备份的问题

perf: 优化动画性能减少卡顿

style: 统一按钮样式和交互体验

docs: 更新版本号显示逻辑

test: 增强错误处理和回滚机制

build: 添加 remark-breaks 依赖
@SummerRay160
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

showDefaultCategory is undefined in the rollback path.

The catch block calls showDefaultCategory(category.id), but this function is never read from useAppStore(). 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 | 🟠 Major

Don't persist the localized display name as the category key.

getAllCategories(...) localizes built-in category names, and this path stores category.name into custom_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 | 🟠 Major

Failed deletes are not rolled back.

deleteCustomCategory(category.id) happens before forceSyncToBackend(), 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 | 🟠 Major

Use the persisted preset filters as the source of truth.

matchesActiveFilters still pulls preset keywords from PRESET_FILTERS, while user-edited preset filters live in assetFilters. That means changes made in AssetFilterManager still 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 | 🟡 Minor

Expose the release-notes expanded state semantically.

The "Release Notes" toggle button is missing aria-expanded and aria-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 | 🟠 Major

Complete 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 | 🟠 Major

Restore still does not apply the backed-up active config state.

For existing configs you keep existing.isActive, and new WebDAV configs are forced to false. A restore can therefore finish with a different active AI/WebDAV profile than the backup describes. The restore path should apply cfg.isActive consistently, and if activeAIConfig / activeWebDAVConfig are 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 | 🟠 Major

Hidden 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 existing categoryOrder array 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 | 🟠 Major

Use a functional progress update in the bulk AI loop.

The per-repo callback closes over a stale analysisProgress value. With concurrency > 1, multiple completions in the same batch can all write the same current + 1, so the progress bar lags or stops advancing. Use setAnalysisProgress(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/catch only helps if indexedDBStorage.removeItem() rejects, but src/services/indexedDbStorage.ts currently swallows IndexedDB errors and resolves void. 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 on title for accessible name.

The Test, Edit, and Delete buttons use only title attributes, which are not reliably announced by screen readers and unavailable on touch devices. Add aria-label for 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 adding name field validation.

The form validates URL, username, password, and path via WebDAVService.validateConfig, but the name field 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 LanguageOption component 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

📥 Commits

Reviewing files that changed from the base of the PR and between b909560 and dbff152.

⛔ Files ignored due to path filters (2)
  • dist/index.html is excluded by !**/dist/**
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • .github/workflows/build-desktop.yml
  • package.json
  • src/App.tsx
  • src/components/AssetFilterManager.tsx
  • src/components/BackToTop.tsx
  • src/components/BulkActionToolbar.tsx
  • src/components/BulkCategorizeModal.tsx
  • src/components/CategorySidebar.tsx
  • src/components/MarkdownRenderer.tsx
  • src/components/ReadmeModal.tsx
  • src/components/ReleaseCard.tsx
  • src/components/ReleaseTimeline.tsx
  • src/components/RepositoryCard.tsx
  • src/components/RepositoryList.tsx
  • src/components/SettingsPanel.tsx
  • src/components/settings/AIConfigPanel.tsx
  • src/components/settings/BackendPanel.tsx
  • src/components/settings/BackupPanel.tsx
  • src/components/settings/CategoryPanel.tsx
  • src/components/settings/DataManagementPanel.tsx
  • src/components/settings/GeneralPanel.tsx
  • src/components/settings/WebDAVPanel.tsx
  • src/components/settings/index.ts
  • src/constants/presetFilters.ts
  • src/index.css
  • src/services/githubApi.ts
  • src/store/useAppStore.ts
  • src/types/index.ts
  • tailwind.config.js

Comment on lines +94 to +149
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.');
}
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +90 to +104
const handleClose = () => {
setIsClosing(true);
setTimeout(() => {
onClose();
setIsClosing(false);
}, 300);
};

const handleDeselectAll = () => {
setIsClosing(true);
setTimeout(() => {
onDeselectAll();
setIsClosing(false);
}, 300);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +82 to +87
if (hasError) {
return (
<span className="text-gray-500 italic">
[图片加载失败: {alt || 'image'}]
</span>
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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:

  1. Add a language prop to MarkdownRendererProps and pass it through, or
  2. Access useAppStore directly in MarkdownImage:
+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.

Suggested change
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}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +510 to +513
// 设置加载状态
setLoading(true);
isAnalyzingRef.current = true;
setAnalysisProgress({ current: 0, total: repos.length });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +89 to +93
await backend.syncRepositories(repositories);
await backend.syncReleases(releases);
await backend.syncAIConfigs(aiConfigs);
await backend.syncWebDAVConfigs(webdavConfigs);
await backend.syncSettings({ hiddenDefaultCategoryIds });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +49 to +67
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);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +153 to +155
id={`mobile-tab-${tab.id}`}
aria-selected={activeTab === tab.id}
aria-controls={`mobile-tabpanel-${tab.id}`}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread src/index.css
Comment on lines +44 to +57
@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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment thread src/store/useAppStore.ts
Comment on lines +272 to +279
// 默认预设筛选器
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' },
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant