diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..5aaf81b --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,149 @@ +# Release 下载功能实现总结 + +## 已完成的更改 + +### 1. 新增组件 + +#### Modal.tsx +- 通用弹窗组件 +- 支持 ESC 键关闭 +- 点击背景关闭 +- 防止页面滚动 + +#### FilterModal.tsx +- 过滤器编辑弹窗 +- 支持新建和编辑过滤器 +- 关键词管理(添加/删除) +- 表单验证 + +#### AssetFilterManager.tsx +- 过滤器管理界面 +- 过滤器列表展示 +- 过滤器激活/取消激活 +- 编辑和删除操作 + +### 2. 类型定义更新 + +#### types/index.ts +```typescript +export interface AssetFilter { + id: string; + name: string; + keywords: string[]; +} +``` + +#### AppState 接口更新 +```typescript +interface AppState { + // ...其他属性 + assetFilters: AssetFilter[]; +} +``` + +### 3. Store 更新 + +#### useAppStore.ts +- 添加 `assetFilters` 状态 +- 添加过滤器管理方法: + - `addAssetFilter` + - `updateAssetFilter` + - `deleteAssetFilter` +- 持久化过滤器配置 + +### 4. ReleaseTimeline 组件重构 + +#### 移除的功能 +- 平台检测逻辑 (`detectPlatforms`) +- 平台图标显示函数 +- 平台颜色映射 +- 平台相关的 UI 组件 + +#### 新增的功能 +- 自定义过滤器集成 +- 改进的下拉列表显示 +- 文件详细信息展示(大小、更新时间、下载次数) +- 整个文件名区域可点击下载 + +#### 过滤逻辑更新 +```typescript +// 旧的平台过滤 +if (selectedPlatforms.length > 0) { + // 基于平台检测的过滤 +} + +// 新的自定义过滤器 +if (selectedFilters.length > 0) { + const activeFilters = assetFilters.filter(filter => + selectedFilters.includes(filter.id) + ); + + filtered = filtered.filter(release => { + const downloadLinks = getDownloadLinks(release); + return downloadLinks.some(link => + activeFilters.some(filter => + filter.keywords.some(keyword => + link.name.toLowerCase().includes(keyword.toLowerCase()) + ) + ) + ); + }); +} +``` + +### 5. UI 改进 + +#### 下拉列表优化 +- 显示文件名、大小、更新时间 +- 整个区域可点击 +- 悬停效果改进 +- 更好的视觉层次 + +#### 过滤器界面 +- 直观的过滤器管理 +- 清晰的激活状态指示 +- 便捷的编辑和删除操作 + +## 功能特点 + +### 1. 灵活的过滤系统 +- 用户可以创建任意数量的自定义过滤器 +- 支持多关键词匹配 +- 可以同时激活多个过滤器 + +### 2. 完整的文件信息 +- 文件名与 GitHub Assets 完全一致 +- 显示文件大小(自动格式化) +- 显示更新时间(相对时间) +- 显示下载统计 + +### 3. 优化的用户体验 +- 点击文件名直接下载 +- 清晰的视觉反馈 +- 响应式设计 +- 无障碍访问支持 + +### 4. 数据持久化 +- 过滤器配置自动保存 +- 跨会话保持用户设置 +- 支持导入导出(通过现有的备份系统) + +## 使用流程 + +1. **创建过滤器** + - 点击"新建过滤器" + - 输入过滤器名称 + - 添加匹配关键词 + - 保存过滤器 + +2. **使用过滤器** + - 在过滤器列表中点击过滤器名称 + - 系统自动筛选匹配的 Release + - 查看筛选结果 + +3. **下载文件** + - 点击 Release 的下载按钮查看文件列表 + - 点击文件名直接下载 + - 查看文件详细信息 + +这个实现完全满足了用户的需求,提供了更灵活、更直观的 Release 文件管理和下载体验。 \ No newline at end of file diff --git a/RELEASE_FILTER_FEATURE.md b/RELEASE_FILTER_FEATURE.md new file mode 100644 index 0000000..53cb7df --- /dev/null +++ b/RELEASE_FILTER_FEATURE.md @@ -0,0 +1,86 @@ +# Release 下载功能更新 + +## 功能概述 + +根据用户需求,我们对 Release 下载功能进行了以下更新: + +### 1. 下拉列表显示所有 Assets +- 下拉列表现在显示 GitHub Release 页面的所有 Assets +- 文件名与 GitHub Assets 的文件名完全一致 +- 不再区分平台,显示所有可用文件 + +### 2. 详细文件信息 +下拉列表中每个文件显示: +- **文件名**: 与 GitHub Assets 完全一致 +- **文件大小**: 格式化显示(B, KB, MB, GB) +- **更新时间**: 相对时间显示(如 "2 days ago") +- **下载次数**: 显示该文件的下载统计 + +### 3. 点击下载 +- 整个文件名区域都可以点击 +- 点击后直接触发下载,打开新标签页 +- 支持鼠标悬停效果,提升用户体验 + +### 4. 自定义过滤器系统 +替换原有的平台筛选,改为更灵活的自定义过滤器: + +#### 过滤器管理 +- **新建过滤器**: 用户可以创建自定义过滤器 +- **编辑过滤器**: 修改现有过滤器的名称和关键词 +- **删除过滤器**: 删除不需要的过滤器 +- **弹窗操作**: 新增和修改都使用弹窗方式 + +#### 过滤器配置 +- **过滤器名称**: 如 "macOS", "Windows", "Linux" 等 +- **关键词匹配**: 支持多个关键词,如: + - macOS 过滤器: ["mac", "dmg", "darwin"] + - Windows 过滤器: ["win", "exe", "msi"] + - Linux 过滤器: ["linux", "deb", "rpm", "appimage"] + +#### 过滤器使用 +- 点击过滤器名称激活/取消激活 +- 激活后,Release 列表只显示包含匹配关键词的文件 +- 支持多个过滤器同时激活 +- 提供清除所有筛选的快捷操作 + +## 技术实现 + +### 新增组件 +1. **FilterModal**: 过滤器编辑弹窗 +2. **AssetFilterManager**: 过滤器管理组件 +3. **Modal**: 通用弹窗组件 + +### 数据结构更新 +```typescript +interface AssetFilter { + id: string; + name: string; + keywords: string[]; +} +``` + +### Store 更新 +- 添加 `assetFilters` 状态 +- 添加过滤器的增删改查操作 +- 持久化过滤器配置 + +### UI 改进 +- 移除平台图标显示 +- 优化下拉列表布局 +- 增强文件信息展示 +- 改进交互体验 + +## 使用示例 + +### 创建 macOS 过滤器 +1. 点击"新建过滤器"按钮 +2. 输入名称: "macOS" +3. 添加关键词: "mac", "dmg", "darwin" +4. 点击"创建" + +### 使用过滤器 +1. 在过滤器列表中点击"macOS" +2. Release 列表自动筛选,只显示包含 macOS 相关文件的 Release +3. 点击文件名直接下载 + +这个更新让用户能够更灵活地筛选和下载所需的文件,提供了更好的用户体验。 \ No newline at end of file diff --git a/src/components/AssetFilterManager.tsx b/src/components/AssetFilterManager.tsx new file mode 100644 index 0000000..01f8f7d --- /dev/null +++ b/src/components/AssetFilterManager.tsx @@ -0,0 +1,156 @@ +import React, { useState } from 'react'; +import { Plus, Edit3, Trash2, Filter } from 'lucide-react'; +import { useAppStore } from '../store/useAppStore'; +import { FilterModal } from './FilterModal'; +import { AssetFilter } from '../types'; + +interface AssetFilterManagerProps { + selectedFilters: string[]; + onFilterToggle: (filterId: string) => void; + onClearFilters: () => void; +} + +export const AssetFilterManager: React.FC = ({ + selectedFilters, + onFilterToggle, + onClearFilters +}) => { + const { assetFilters, addAssetFilter, updateAssetFilter, deleteAssetFilter, language } = useAppStore(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingFilter, setEditingFilter] = useState(); + + const handleCreateFilter = () => { + setEditingFilter(undefined); + setIsModalOpen(true); + }; + + const handleEditFilter = (filter: AssetFilter) => { + setEditingFilter(filter); + setIsModalOpen(true); + }; + + const handleDeleteFilter = (filterId: string) => { + if (confirm(language === 'zh' ? '确定要删除这个过滤器吗?' : 'Are you sure you want to delete this filter?')) { + deleteAssetFilter(filterId); + // Remove from selected filters if it was selected + if (selectedFilters.includes(filterId)) { + onFilterToggle(filterId); + } + } + }; + + const handleSaveFilter = (filter: AssetFilter) => { + if (editingFilter) { + updateAssetFilter(filter.id, filter); + } else { + addAssetFilter(filter); + } + }; + + const t = (zh: string, en: string) => language === 'zh' ? zh : en; + + return ( +
+ {/* Header */} +
+
+ +

+ {t('自定义过滤器', 'Custom Filters')} +

+
+ +
+ + {/* Filters List */} + {assetFilters.length > 0 ? ( +
+
+ {assetFilters.map(filter => ( +
+ + +
+ + +
+
+ ))} +
+ + {selectedFilters.length > 0 && ( +
+ + {t(`已选择 ${selectedFilters.length} 个过滤器`, `${selectedFilters.length} filters selected`)} + + +
+ )} +
+ ) : ( +
+ +

+ {t('暂无自定义过滤器', 'No Custom Filters')} +

+

+ {t('创建过滤器来快速筛选特定类型的文件', 'Create filters to quickly find specific types of files')} +

+ +
+ )} + + {/* Filter Modal */} + setIsModalOpen(false)} + filter={editingFilter} + onSave={handleSaveFilter} + /> +
+ ); +}; \ No newline at end of file diff --git a/src/components/FilterModal.tsx b/src/components/FilterModal.tsx new file mode 100644 index 0000000..4795d5d --- /dev/null +++ b/src/components/FilterModal.tsx @@ -0,0 +1,169 @@ +import React, { useState, useEffect } from 'react'; +import { X, Plus, Trash2 } from 'lucide-react'; +import { Modal } from './Modal'; +import { AssetFilter } from '../types'; + +interface FilterModalProps { + isOpen: boolean; + onClose: () => void; + filter?: AssetFilter; + onSave: (filter: AssetFilter) => void; +} + +export const FilterModal: React.FC = ({ + isOpen, + onClose, + filter, + onSave +}) => { + const [name, setName] = useState(''); + const [keywords, setKeywords] = useState([]); + const [newKeyword, setNewKeyword] = useState(''); + + useEffect(() => { + if (filter) { + setName(filter.name); + setKeywords([...filter.keywords]); + } else { + setName(''); + setKeywords([]); + } + setNewKeyword(''); + }, [filter, isOpen]); + + const handleAddKeyword = () => { + const trimmed = newKeyword.trim(); + if (trimmed && !keywords.includes(trimmed)) { + setKeywords([...keywords, trimmed]); + setNewKeyword(''); + } + }; + + const handleRemoveKeyword = (index: number) => { + setKeywords(keywords.filter((_, i) => i !== index)); + }; + + const handleSave = () => { + if (!name.trim() || keywords.length === 0) { + return; + } + + const savedFilter: AssetFilter = { + id: filter?.id || Date.now().toString(), + name: name.trim(), + keywords: keywords.filter(k => k.trim()) + }; + + onSave(savedFilter); + onClose(); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddKeyword(); + } + }; + + return ( + +
+ {/* Filter Name */} +
+ + setName(e.target.value)} + placeholder="例如: macOS" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white" + /> +
+ + {/* Keywords */} +
+ + + {/* Add keyword input */} +
+ setNewKeyword(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="输入关键词,如: mac, dmg" + className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white" + /> + +
+ + {/* Keywords list */} + {keywords.length > 0 && ( +
+

+ 已添加的关键词: +

+
+ {keywords.map((keyword, index) => ( +
+ {keyword} + +
+ ))} +
+
+ )} + + {keywords.length === 0 && ( +

+ 请添加至少一个关键词用于匹配文件名 +

+ )} +
+ + {/* Help text */} +
+

+ 提示: 关键词将用于匹配 GitHub Release 中的文件名。例如,添加 "mac" 和 "dmg" 关键词可以匹配包含这些字符的文件。 +

+
+ + {/* Action buttons */} +
+ + +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 70eb066..a3befe6 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { X } from 'lucide-react'; interface ModalProps { @@ -9,13 +9,33 @@ interface ModalProps { maxWidth?: string; } -export const Modal: React.FC = ({ - isOpen, - onClose, - title, - children, - maxWidth = 'max-w-md' +export const Modal: React.FC = ({ + isOpen, + onClose, + title, + children, + maxWidth = 'max-w-md' }) => { + // Close modal on Escape key press + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener('keydown', handleEscape); + // Prevent body scroll when modal is open + document.body.style.overflow = 'hidden'; + } + + return () => { + document.removeEventListener('keydown', handleEscape); + document.body.style.overflow = 'unset'; + }; + }, [isOpen, onClose]); + if (!isOpen) return null; return ( @@ -28,7 +48,10 @@ export const Modal: React.FC = ({ {/* Modal */}
-
+
e.stopPropagation()} + > {/* Header */}

diff --git a/src/components/ReleaseTimeline.tsx b/src/components/ReleaseTimeline.tsx index dc12b08..bf5b707 100644 --- a/src/components/ReleaseTimeline.tsx +++ b/src/components/ReleaseTimeline.tsx @@ -1,9 +1,10 @@ import React, { useState, useMemo, useEffect } from 'react'; -import { ExternalLink, GitBranch, Calendar, Package, Bell, Search, X, RefreshCw, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, EyeOff, Apple, Monitor, Terminal, Smartphone, Globe, Download, ChevronDown } from 'lucide-react'; +import { ExternalLink, GitBranch, Calendar, Package, Bell, Search, X, RefreshCw, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, EyeOff, Download, ChevronDown } from 'lucide-react'; import { Release } from '../types'; import { useAppStore } from '../store/useAppStore'; import { GitHubApiService } from '../services/githubApi'; import { formatDistanceToNow, format } from 'date-fns'; +import { AssetFilterManager } from './AssetFilterManager'; export const ReleaseTimeline: React.FC = () => { const { @@ -13,13 +14,14 @@ export const ReleaseTimeline: React.FC = () => { readReleases, githubToken, language, + assetFilters, setReleases, addReleases, markReleaseAsRead, } = useAppStore(); const [searchQuery, setSearchQuery] = useState(''); - const [selectedPlatforms, setSelectedPlatforms] = useState([]); + const [selectedFilters, setSelectedFilters] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); const [lastRefreshTime, setLastRefreshTime] = useState(null); const [currentPage, setCurrentPage] = useState(1); @@ -64,74 +66,17 @@ export const ReleaseTimeline: React.FC = () => { }); }; - // Enhanced platform detection based on the userscript - const detectPlatforms = (filename: string): string[] => { - const name = filename.toLowerCase(); - const platforms: string[] = []; - // Platform detection rules based on the userscript - const platformRules = { - windows: [ - '.exe', '.msi', '.zip', '.7z', - 'windows', 'win32', 'win64', 'win-x64', 'win-x86', 'win-arm64', - '-win.', '.win.', '-windows.', '.windows.', - 'setup', 'installer' - ], - macos: [ - '.dmg', '.pkg', '.app.zip', - 'darwin', 'macos', 'mac-os', 'osx', 'mac-universal', - '-mac.', '.mac.', '-macos.', '.macos.', '-darwin.', '.darwin.', - 'universal', 'x86_64-apple', 'arm64-apple' - ], - linux: [ - '.deb', '.rpm', '.tar.gz', '.tar.xz', '.tar.bz2', '.appimage', - 'linux', 'ubuntu', 'debian', 'fedora', 'centos', 'arch', 'alpine', - '-linux.', '.linux.', 'x86_64-unknown-linux', 'aarch64-unknown-linux', - 'musl', 'gnu' - ], - android: [ - '.apk', '.aab', - 'android', '-android.', '.android.', - 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64' - ], - ios: [ - '.ipa', - 'ios', '-ios.', '.ios.', - 'iphone', 'ipad' - ] - }; - - // Check each platform - Object.entries(platformRules).forEach(([platform, keywords]) => { - if (keywords.some(keyword => name.includes(keyword))) { - platforms.push(platform); - } - }); - - // Special handling for universal files - if (platforms.length === 0) { - // Check for source code or universal packages - if (name.includes('source') || name.includes('src') || - name.includes('universal') || name.includes('all') || - name.match(/\.(zip|tar\.gz|tar\.xz)$/) && !name.includes('win') && !name.includes('mac') && !name.includes('linux')) { - platforms.push('universal'); - } - } - - return platforms.length > 0 ? platforms : ['universal']; - }; const getDownloadLinks = (release: Release) => { - const links: Array<{ name: string; url: string; platforms: string[]; size: number; downloadCount: number }> = []; + const links: Array<{ name: string; url: string; size: number; downloadCount: number }> = []; // Use GitHub release assets (this is the correct way to get downloads) if (release.assets && release.assets.length > 0) { release.assets.forEach(asset => { - const platforms = detectPlatforms(asset.name); links.push({ name: asset.name, url: asset.browser_download_url, - platforms, size: asset.size, downloadCount: asset.download_count }); @@ -147,10 +92,9 @@ export const ReleaseTimeline: React.FC = () => { if (url.includes('/download/') || url.includes('/releases/') || name.toLowerCase().includes('download') || /\.(exe|dmg|deb|rpm|apk|ipa|zip|tar\.gz|msi|pkg|appimage)$/i.test(url)) { - const platforms = detectPlatforms(name + ' ' + url); // Avoid duplicates with assets if (!links.some(link => link.url === url || link.name === name)) { - links.push({ name, url, platforms, size: 0, downloadCount: 0 }); + links.push({ name, url, size: 0, downloadCount: 0 }); } } } @@ -163,7 +107,7 @@ export const ReleaseTimeline: React.FC = () => { releaseSubscriptions.has(release.repository.id) ); - // Apply search and platform filters + // Apply search and custom filters const filteredReleases = useMemo(() => { let filtered = subscribedReleases; @@ -179,12 +123,19 @@ export const ReleaseTimeline: React.FC = () => { ); } - // Platform filter - if (selectedPlatforms.length > 0) { + // Custom asset filters + if (selectedFilters.length > 0) { + const activeFilters = assetFilters.filter(filter => selectedFilters.includes(filter.id)); + filtered = filtered.filter(release => { const downloadLinks = getDownloadLinks(release); + return downloadLinks.some(link => - selectedPlatforms.some(platform => link.platforms.includes(platform)) + activeFilters.some(filter => + filter.keywords.some(keyword => + link.name.toLowerCase().includes(keyword.toLowerCase()) + ) + ) ); }); } @@ -192,24 +143,27 @@ export const ReleaseTimeline: React.FC = () => { return filtered.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime() ); - }, [subscribedReleases, searchQuery, selectedPlatforms]); + }, [subscribedReleases, searchQuery, selectedFilters, assetFilters]); // Pagination const totalPages = Math.ceil(filteredReleases.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const paginatedReleases = filteredReleases.slice(startIndex, startIndex + itemsPerPage); - // Get available platforms from all releases - const availablePlatforms = useMemo(() => { - const platforms = new Set(); - subscribedReleases.forEach(release => { - const downloadLinks = getDownloadLinks(release); - downloadLinks.forEach(link => { - link.platforms.forEach(platform => platforms.add(platform)); - }); - }); - return Array.from(platforms).sort(); - }, [subscribedReleases]); + // Filter handlers + const handleFilterToggle = (filterId: string) => { + setSelectedFilters(prev => + prev.includes(filterId) + ? prev.filter(id => id !== filterId) + : [...prev, filterId] + ); + setCurrentPage(1); // Reset to first page when filtering + }; + + const handleClearFilters = () => { + setSelectedFilters([]); + setCurrentPage(1); + }; const handleRefresh = async () => { if (!githubToken) { @@ -286,18 +240,9 @@ export const ReleaseTimeline: React.FC = () => { } }; - const handlePlatformToggle = (platform: string) => { - setSelectedPlatforms(prev => - prev.includes(platform) - ? prev.filter(p => p !== platform) - : [...prev, platform] - ); - setCurrentPage(1); // Reset to first page when filtering - }; - - const clearFilters = () => { + const clearAllFilters = () => { setSearchQuery(''); - setSelectedPlatforms([]); + setSelectedFilters([]); setCurrentPage(1); }; @@ -331,51 +276,7 @@ export const ReleaseTimeline: React.FC = () => { return rangeWithDots; }; - const getPlatformIcon = (platform: string) => { - const platformLower = platform.toLowerCase(); - - switch (platformLower) { - case 'windows': - return Monitor; - case 'macos': - case 'mac': - case 'ios': - return Apple; - case 'linux': - return Terminal; - case 'android': - return Smartphone; - case 'universal': - default: - return Download; - } - }; - const getPlatformDisplayName = (platform: string) => { - const platformLower = platform.toLowerCase(); - const nameMap: Record = { - windows: 'Windows', - macos: 'macOS', - mac: 'macOS', - linux: 'Linux', - android: 'Android', - ios: 'iOS', - universal: 'Universal' - }; - return nameMap[platformLower] || platform; - }; - - const getPlatformColor = (platform: string) => { - const colorMap: Record = { - windows: 'text-blue-600 dark:text-blue-400', - macos: 'text-gray-600 dark:text-gray-400', - linux: 'text-yellow-600 dark:text-yellow-400', - android: 'text-green-600 dark:text-green-400', - ios: 'text-gray-600 dark:text-gray-400', - universal: 'text-purple-600 dark:text-purple-400' - }; - return colorMap[platform] || 'text-gray-600 dark:text-gray-400'; - }; const truncateBody = (body: string, maxLength = 200) => { if (body.length <= maxLength) return body; @@ -529,35 +430,23 @@ export const ReleaseTimeline: React.FC = () => { )}

- {/* Platform Filters */} - {availablePlatforms.length > 0 && ( -
- - {t('平台:', 'Platforms:')} - - {availablePlatforms.map(platform => ( - - ))} - {(searchQuery || selectedPlatforms.length > 0) && ( - - )} + {/* Custom Asset Filters */} + + + {/* Clear All Filters */} + {(searchQuery || selectedFilters.length > 0) && ( +
+
)}
@@ -571,7 +460,7 @@ export const ReleaseTimeline: React.FC = () => { `Showing ${startIndex + 1}-${Math.min(startIndex + itemsPerPage, filteredReleases.length)} of ${filteredReleases.length} releases` )} - {(searchQuery || selectedPlatforms.length > 0) && ( + {(searchQuery || selectedFilters.length > 0) && ( ({t('已筛选', 'filtered')}) @@ -732,48 +621,44 @@ export const ReleaseTimeline: React.FC = () => {
{openDropdowns.has(release.id) && ( -
- {downloadLinks.map((link, index) => ( - { - e.stopPropagation(); - handleReleaseClick(release.id); - toggleDropdown(release.id); - }} - > -
-
- {link.platforms.map((platform, pIndex) => { - const IconComponent = getPlatformIcon(platform); - return ( - - ); - })} -
+
)}
@@ -852,47 +737,44 @@ export const ReleaseTimeline: React.FC = () => { {openDropdowns.has(release.id) && ( -
- {downloadLinks.map((link, index) => ( - { - e.stopPropagation(); - handleReleaseClick(release.id); - toggleDropdown(release.id); - }} - > - diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 2a54432..3e21cdf 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -import { AppState, Repository, Release, AIConfig, WebDAVConfig, SearchFilters, GitHubUser, Category } from '../types'; +import { AppState, Repository, Release, AIConfig, WebDAVConfig, SearchFilters, GitHubUser, Category, AssetFilter } from '../types'; interface AppActions { // Auth actions @@ -43,6 +43,11 @@ interface AppActions { updateCustomCategory: (id: string, updates: Partial) => void; deleteCustomCategory: (id: string) => void; + // Asset Filter actions + addAssetFilter: (filter: AssetFilter) => void; + updateAssetFilter: (id: string, updates: Partial) => void; + deleteAssetFilter: (id: string) => void; + // UI actions setTheme: (theme: 'light' | 'dark') => void; setCurrentView: (view: 'repositories' | 'releases' | 'settings') => void; @@ -168,6 +173,7 @@ export const useAppStore = create()( releaseSubscriptions: new Set(), readReleases: new Set(), customCategories: [], + assetFilters: [], theme: 'light', currentView: 'repositories', language: 'zh', @@ -284,6 +290,19 @@ export const useAppStore = create()( customCategories: state.customCategories.filter(category => category.id !== id) })), + // Asset Filter actions + addAssetFilter: (filter) => set((state) => ({ + assetFilters: [...state.assetFilters, filter] + })), + updateAssetFilter: (id, updates) => set((state) => ({ + assetFilters: state.assetFilters.map(filter => + filter.id === id ? { ...filter, ...updates } : filter + ) + })), + deleteAssetFilter: (id) => set((state) => ({ + assetFilters: state.assetFilters.filter(filter => filter.id !== id) + })), + // UI actions setTheme: (theme) => set({ theme }), setCurrentView: (currentView) => set({ currentView }), @@ -318,6 +337,9 @@ export const useAppStore = create()( // 持久化自定义分类 customCategories: state.customCategories, + // 持久化资源过滤器 + assetFilters: state.assetFilters, + // 持久化UI设置 theme: state.theme, language: state.language, @@ -361,6 +383,11 @@ export const useAppStore = create()( state.customCategories = []; } + // 初始化资源过滤器 + if (!state.assetFilters) { + state.assetFilters = []; + } + console.log('Store rehydrated:', { isAuthenticated: state.isAuthenticated, repositoriesCount: state.repositories?.length || 0, diff --git a/src/types/index.ts b/src/types/index.ts index 6d408eb..94f6f33 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -105,6 +105,12 @@ export interface Category { isCustom?: boolean; } +export interface AssetFilter { + id: string; + name: string; + keywords: string[]; +} + export interface AppState { // Auth user: GitHubUser | null; @@ -137,6 +143,9 @@ export interface AppState { // Categories customCategories: Category[]; // 新增:自定义分类 + // Asset Filters + assetFilters: AssetFilter[]; // 新增:资源过滤器 + // UI theme: 'light' | 'dark'; currentView: 'repositories' | 'releases' | 'settings';