mirror of
https://github.com/AmintaCCCP/GithubStarsManager.git
synced 2025-11-25 02:34:54 +08:00
0.1.1
This commit is contained in:
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -105,7 +105,7 @@ export const Header: React.FC = () => {
|
|||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="flex items-center justify-center w-10 h-10 rounded-lg overflow-hidden">
|
<div className="flex items-center justify-center w-10 h-10 rounded-lg overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src="/assets/icon.png"
|
src="/icon.png"
|
||||||
alt="GitHub Stars Manager"
|
alt="GitHub Stars Manager"
|
||||||
className="w-10 h-10 object-cover"
|
className="w-10 h-10 object-cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useMemo } from '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 } from 'lucide-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 { Release } from '../types';
|
||||||
import { useAppStore } from '../store/useAppStore';
|
import { useAppStore } from '../store/useAppStore';
|
||||||
import { GitHubApiService } from '../services/githubApi';
|
import { GitHubApiService } from '../services/githubApi';
|
||||||
@@ -25,6 +25,44 @@ export const ReleaseTimeline: React.FC = () => {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(100);
|
const [itemsPerPage, setItemsPerPage] = useState(100);
|
||||||
const [viewMode, setViewMode] = useState<'compact' | 'detailed'>('compact');
|
const [viewMode, setViewMode] = useState<'compact' | 'detailed'>('compact');
|
||||||
|
const [openDropdowns, setOpenDropdowns] = useState<Set<number>>(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
|
// Enhanced platform detection based on the userscript
|
||||||
const detectPlatforms = (filename: string): string[] => {
|
const detectPlatforms = (filename: string): string[] => {
|
||||||
@@ -84,10 +122,24 @@ export const ReleaseTimeline: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getDownloadLinks = (release: Release) => {
|
const getDownloadLinks = (release: Release) => {
|
||||||
// Extract download links from release body
|
const links: Array<{ name: string; url: string; platforms: string[]; size: number; downloadCount: number }> = [];
|
||||||
const downloadRegex = /\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g;
|
|
||||||
const links: Array<{ name: string; url: string; platforms: string[] }> = [];
|
|
||||||
|
|
||||||
|
// 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;
|
let match;
|
||||||
while ((match = downloadRegex.exec(release.body)) !== null) {
|
while ((match = downloadRegex.exec(release.body)) !== null) {
|
||||||
const [, name, url] = match;
|
const [, name, url] = match;
|
||||||
@@ -96,18 +148,10 @@ export const ReleaseTimeline: React.FC = () => {
|
|||||||
name.toLowerCase().includes('download') ||
|
name.toLowerCase().includes('download') ||
|
||||||
/\.(exe|dmg|deb|rpm|apk|ipa|zip|tar\.gz|msi|pkg|appimage)$/i.test(url)) {
|
/\.(exe|dmg|deb|rpm|apk|ipa|zip|tar\.gz|msi|pkg|appimage)$/i.test(url)) {
|
||||||
const platforms = detectPlatforms(name + ' ' + url);
|
const platforms = detectPlatforms(name + ' ' + url);
|
||||||
links.push({ name, 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 });
|
||||||
|
}
|
||||||
// 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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,42 +711,71 @@ export const ReleaseTimeline: React.FC = () => {
|
|||||||
</h5>
|
</h5>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Download Links */}
|
{/* Download Links - Dropdown */}
|
||||||
{downloadLinks.length > 0 && (
|
{downloadLinks.length > 0 && (
|
||||||
<div className="mb-4">
|
<div className="mb-4 relative download-dropdown">
|
||||||
<h6 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
{t('下载:', 'Downloads:')}
|
<h6 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
</h6>
|
{t('下载:', 'Downloads:')} ({downloadLinks.length})
|
||||||
<div className="flex flex-wrap gap-2">
|
</h6>
|
||||||
{downloadLinks.map((link, index) => (
|
<button
|
||||||
<a
|
onClick={(e) => {
|
||||||
key={index}
|
e.stopPropagation();
|
||||||
href={link.url}
|
toggleDropdown(release.id);
|
||||||
target="_blank"
|
}}
|
||||||
rel="noopener noreferrer"
|
className="flex items-center space-x-1 px-3 py-1.5 bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors text-sm"
|
||||||
className="flex items-center space-x-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors text-sm"
|
>
|
||||||
title={link.name}
|
<Download className="w-4 h-4" />
|
||||||
onClick={(e) => {
|
<span>{t('查看下载', 'View Downloads')}</span>
|
||||||
e.stopPropagation();
|
<ChevronDown className={`w-4 h-4 transition-transform ${openDropdowns.has(release.id) ? 'rotate-180' : ''}`} />
|
||||||
handleReleaseClick(release.id);
|
</button>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
{link.platforms.map((platform, pIndex) => {
|
|
||||||
const IconComponent = getPlatformIcon(platform);
|
|
||||||
return (
|
|
||||||
<IconComponent
|
|
||||||
key={pIndex}
|
|
||||||
className={`w-4 h-4 ${getPlatformColor(platform)}`}
|
|
||||||
title={getPlatformDisplayName(platform)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<span className="truncate max-w-32">{link.name}</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{openDropdowns.has(release.id) && (
|
||||||
|
<div className="absolute z-10 w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-64 overflow-y-auto">
|
||||||
|
{downloadLinks.map((link, index) => (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
href={link.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-between px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors border-b border-gray-100 dark:border-gray-600 last:border-b-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleReleaseClick(release.id);
|
||||||
|
toggleDropdown(release.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3 min-w-0 flex-1">
|
||||||
|
<div className="flex items-center space-x-1 flex-shrink-0">
|
||||||
|
{link.platforms.map((platform, pIndex) => {
|
||||||
|
const IconComponent = getPlatformIcon(platform);
|
||||||
|
return (
|
||||||
|
<IconComponent
|
||||||
|
key={pIndex}
|
||||||
|
className={`w-4 h-4 ${getPlatformColor(platform)}`}
|
||||||
|
title={getPlatformDisplayName(platform)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{link.name}
|
||||||
|
</div>
|
||||||
|
{link.size > 0 && (
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{formatFileSize(link.size)}
|
||||||
|
{link.downloadCount > 0 && ` • ${link.downloadCount.toLocaleString()} ${t('下载', 'downloads')}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Download className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -760,44 +833,67 @@ export const ReleaseTimeline: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Download Links - 横向排列,可换行 */}
|
{/* Download Links - Dropdown */}
|
||||||
<div className="col-span-4 min-w-0">
|
<div className="col-span-4 min-w-0 relative download-dropdown">
|
||||||
{downloadLinks.length > 0 ? (
|
{downloadLinks.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="relative">
|
||||||
{downloadLinks.slice(0, 6).map((link, index) => (
|
<button
|
||||||
<a
|
onClick={(e) => {
|
||||||
key={index}
|
e.stopPropagation();
|
||||||
href={link.url}
|
toggleDropdown(release.id);
|
||||||
target="_blank"
|
}}
|
||||||
rel="noopener noreferrer"
|
className="flex items-center space-x-2 px-3 py-1.5 bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-lg hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors text-sm w-full justify-between"
|
||||||
className="flex items-center space-x-1 px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
>
|
||||||
title={`${link.name} (${link.platforms.join(', ')})`}
|
<div className="flex items-center space-x-2">
|
||||||
onClick={(e) => {
|
<Download className="w-4 h-4" />
|
||||||
e.stopPropagation();
|
<span>{downloadLinks.length} {t('个文件', 'files')}</span>
|
||||||
handleReleaseClick(release.id);
|
</div>
|
||||||
}}
|
<ChevronDown className={`w-4 h-4 transition-transform ${openDropdowns.has(release.id) ? 'rotate-180' : ''}`} />
|
||||||
>
|
</button>
|
||||||
<div className="flex items-center space-x-0.5">
|
|
||||||
{link.platforms.map((platform, pIndex) => {
|
{openDropdowns.has(release.id) && (
|
||||||
const IconComponent = getPlatformIcon(platform);
|
<div className="absolute z-20 left-0 right-0 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-48 overflow-y-auto">
|
||||||
return (
|
{downloadLinks.map((link, index) => (
|
||||||
<IconComponent
|
<a
|
||||||
key={pIndex}
|
key={index}
|
||||||
className={`w-3 h-3 ${getPlatformColor(platform)}`}
|
href={link.url}
|
||||||
title={getPlatformDisplayName(platform)}
|
target="_blank"
|
||||||
/>
|
rel="noopener noreferrer"
|
||||||
);
|
className="flex items-center justify-between px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors border-b border-gray-100 dark:border-gray-600 last:border-b-0"
|
||||||
})}
|
onClick={(e) => {
|
||||||
</div>
|
e.stopPropagation();
|
||||||
<span className="text-xs text-gray-700 dark:text-gray-300 truncate max-w-16">
|
handleReleaseClick(release.id);
|
||||||
{link.name.split('.').pop() || link.name}
|
toggleDropdown(release.id);
|
||||||
</span>
|
}}
|
||||||
</a>
|
>
|
||||||
))}
|
<div className="flex items-center space-x-2 min-w-0 flex-1">
|
||||||
{downloadLinks.length > 6 && (
|
<div className="flex items-center space-x-1 flex-shrink-0">
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400 px-2 py-1">
|
{link.platforms.slice(0, 2).map((platform, pIndex) => {
|
||||||
+{downloadLinks.length - 6}
|
const IconComponent = getPlatformIcon(platform);
|
||||||
</span>
|
return (
|
||||||
|
<IconComponent
|
||||||
|
key={pIndex}
|
||||||
|
className={`w-3 h-3 ${getPlatformColor(platform)}`}
|
||||||
|
title={getPlatformDisplayName(platform)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="text-xs font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{link.name}
|
||||||
|
</div>
|
||||||
|
{link.size > 0 && (
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{formatFileSize(link.size)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Download className="w-3 h-3 text-gray-400 flex-shrink-0" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export class GitHubApiService {
|
|||||||
body: release.body || '',
|
body: release.body || '',
|
||||||
published_at: release.published_at,
|
published_at: release.published_at,
|
||||||
html_url: release.html_url,
|
html_url: release.html_url,
|
||||||
|
assets: release.assets || [],
|
||||||
repository: {
|
repository: {
|
||||||
id: 0, // Will be set by caller
|
id: 0, // Will be set by caller
|
||||||
full_name: `${owner}/${repo}`,
|
full_name: `${owner}/${repo}`,
|
||||||
@@ -146,6 +147,7 @@ export class GitHubApiService {
|
|||||||
body: release.body || '',
|
body: release.body || '',
|
||||||
published_at: release.published_at,
|
published_at: release.published_at,
|
||||||
html_url: release.html_url,
|
html_url: release.html_url,
|
||||||
|
assets: release.assets || [],
|
||||||
repository: {
|
repository: {
|
||||||
id: 0, // Will be set by caller
|
id: 0, // Will be set by caller
|
||||||
full_name: `${owner}/${repo}`,
|
full_name: `${owner}/${repo}`,
|
||||||
|
|||||||
@@ -27,6 +27,17 @@ export interface Repository {
|
|||||||
last_edited?: string;
|
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 {
|
export interface Release {
|
||||||
id: number;
|
id: number;
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
@@ -34,6 +45,7 @@ export interface Release {
|
|||||||
body: string;
|
body: string;
|
||||||
published_at: string;
|
published_at: string;
|
||||||
html_url: string;
|
html_url: string;
|
||||||
|
assets: ReleaseAsset[];
|
||||||
repository: {
|
repository: {
|
||||||
id: number;
|
id: number;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user