diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..86667f8 Binary files /dev/null and b/public/icon.png differ diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 1173e06..7debdaa 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -105,7 +105,7 @@ export const Header: React.FC = () => {
GitHub Stars Manager diff --git a/src/components/ReleaseTimeline.tsx b/src/components/ReleaseTimeline.tsx index 4254c75..dc12b08 100644 --- a/src/components/ReleaseTimeline.tsx +++ b/src/components/ReleaseTimeline.tsx @@ -1,5 +1,5 @@ -import React, { useState, useMemo } from 'react'; -import { ExternalLink, GitBranch, Calendar, Package, Bell, Search, X, RefreshCw, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, EyeOff, Apple, Monitor, Terminal, Smartphone, Globe, Download } from 'lucide-react'; +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 { Release } from '../types'; import { useAppStore } from '../store/useAppStore'; import { GitHubApiService } from '../services/githubApi'; @@ -25,6 +25,44 @@ export const ReleaseTimeline: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(100); const [viewMode, setViewMode] = useState<'compact' | 'detailed'>('compact'); + const [openDropdowns, setOpenDropdowns] = useState>(new Set()); + + // Close dropdowns when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + if (!target.closest('.download-dropdown')) { + setOpenDropdowns(new Set()); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + // Format file size helper function + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + }; + + // Toggle dropdown for a specific release + const toggleDropdown = (releaseId: number) => { + setOpenDropdowns(prev => { + const newSet = new Set(prev); + if (newSet.has(releaseId)) { + newSet.delete(releaseId); + } else { + newSet.add(releaseId); + } + return newSet; + }); + }; // Enhanced platform detection based on the userscript const detectPlatforms = (filename: string): string[] => { @@ -84,10 +122,24 @@ export const ReleaseTimeline: React.FC = () => { }; const getDownloadLinks = (release: Release) => { - // Extract download links from release body - const downloadRegex = /\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g; - const links: Array<{ name: string; url: string; platforms: string[] }> = []; + const links: Array<{ name: string; url: string; platforms: 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 + }); + }); + } + + // Fallback: Extract download links from release body (for custom links) + const downloadRegex = /\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g; let match; while ((match = downloadRegex.exec(release.body)) !== null) { const [, name, url] = match; @@ -96,18 +148,10 @@ export const ReleaseTimeline: React.FC = () => { name.toLowerCase().includes('download') || /\.(exe|dmg|deb|rpm|apk|ipa|zip|tar\.gz|msi|pkg|appimage)$/i.test(url)) { const platforms = detectPlatforms(name + ' ' + url); - links.push({ name, url, platforms }); - } - } - - // Also check for GitHub release assets pattern - const assetRegex = /https:\/\/github\.com\/[^\/]+\/[^\/]+\/releases\/download\/[^\/]+\/([^\s\)]+)/g; - while ((match = assetRegex.exec(release.body)) !== null) { - const [url, filename] = match; - const platforms = detectPlatforms(filename); - // Avoid duplicates - if (!links.some(link => link.url === url)) { - links.push({ name: filename, url, platforms }); + // Avoid duplicates with assets + if (!links.some(link => link.url === url || link.name === name)) { + links.push({ name, url, platforms, size: 0, downloadCount: 0 }); + } } } @@ -667,42 +711,71 @@ export const ReleaseTimeline: React.FC = () => { )} - {/* Download Links */} + {/* Download Links - Dropdown */} {downloadLinks.length > 0 && ( -
-
- {t('下载:', 'Downloads:')} -
-
- {downloadLinks.map((link, index) => ( - { - e.stopPropagation(); - handleReleaseClick(release.id); - }} - > -
- {link.platforms.map((platform, pIndex) => { - const IconComponent = getPlatformIcon(platform); - return ( - - ); - })} -
- {link.name} -
- ))} +
+
+
+ {t('下载:', 'Downloads:')} ({downloadLinks.length}) +
+
+ + {openDropdowns.has(release.id) && ( + + )}
)} @@ -760,44 +833,67 @@ export const ReleaseTimeline: React.FC = () => {

- {/* Download Links - 横向排列,可换行 */} -
+ {/* Download Links - Dropdown */} +
{downloadLinks.length > 0 ? ( -
- {downloadLinks.slice(0, 6).map((link, index) => ( - { - e.stopPropagation(); - handleReleaseClick(release.id); - }} - > -
- {link.platforms.map((platform, pIndex) => { - const IconComponent = getPlatformIcon(platform); - return ( - - ); - })} -
- - {link.name.split('.').pop() || link.name} - -
- ))} - {downloadLinks.length > 6 && ( - - +{downloadLinks.length - 6} - +
+ + + {openDropdowns.has(release.id) && ( + )}
) : ( diff --git a/src/services/githubApi.ts b/src/services/githubApi.ts index 2a32dc9..97f42cc 100644 --- a/src/services/githubApi.ts +++ b/src/services/githubApi.ts @@ -91,6 +91,7 @@ export class GitHubApiService { body: release.body || '', published_at: release.published_at, html_url: release.html_url, + assets: release.assets || [], repository: { id: 0, // Will be set by caller full_name: `${owner}/${repo}`, @@ -146,6 +147,7 @@ export class GitHubApiService { body: release.body || '', published_at: release.published_at, html_url: release.html_url, + assets: release.assets || [], repository: { id: 0, // Will be set by caller full_name: `${owner}/${repo}`, diff --git a/src/types/index.ts b/src/types/index.ts index cb47045..6d408eb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -27,6 +27,17 @@ export interface Repository { last_edited?: string; } +export interface ReleaseAsset { + id: number; + name: string; + size: number; + download_count: number; + browser_download_url: string; + content_type: string; + created_at: string; + updated_at: string; +} + export interface Release { id: number; tag_name: string; @@ -34,6 +45,7 @@ export interface Release { body: string; published_at: string; html_url: string; + assets: ReleaseAsset[]; repository: { id: number; full_name: string;