
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:')}
-
-
- {/* 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;