Files
CeruMusic/website/script.js
2025-09-28 22:19:45 +08:00

960 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Smooth scrolling functions
function scrollToDownload() {
document.getElementById('download').scrollIntoView({
behavior: 'smooth'
})
}
function scrollToFeatures() {
document.getElementById('features').scrollIntoView({
behavior: 'smooth'
})
}
// GitHub repository configuration
const GITHUB_REPO = 'timeshiftsauce/CeruMusic'
const GITHUB_PROXY = 'https://gh-proxy.com/'
const GITHUB_API_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`
const GITHUB_RELEASES_API_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases`
// Cache for release data
let releaseData = null
let releaseDataTimestamp = null
let allReleasesData = null
let allReleasesTimestamp = null
const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes
// Get all releases from GitHub API
async function getAllReleases() {
// Check cache first
const now = Date.now()
if (allReleasesData && allReleasesTimestamp && now - allReleasesTimestamp < CACHE_DURATION) {
return allReleasesData
}
try {
const response = await fetch(GITHUB_RELEASES_API_URL)
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`)
}
const data = await response.json()
// Filter and sort releases by version
const releases = data
.filter(release => !release.draft && !release.prerelease)
.sort((a, b) => compareVersions(b.tag_name, a.tag_name))
// Cache the data
allReleasesData = releases
allReleasesTimestamp = now
return releases
} catch (error) {
console.error('Failed to fetch releases data:', error)
return []
}
}
// Download functionality
async function downloadApp(platform) {
const button = event.target
const originalText = button.innerHTML
// Show loading state
button.innerHTML = `
<svg class="btn-icon spinning" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 12a9 9 0 11-6.219-8.56"/>
</svg>
获取下载链接...
`
button.disabled = true
try {
// Detect user's architecture for better matching
const userArch = detectArchitecture()
// Get latest release from GitHub
const release = await getLatestRelease()
if (!release) {
throw new Error('无法获取最新版本信息')
}
const downloadUrl = findDownloadAsset(release.assets, platform, userArch)
if (!downloadUrl) {
throw new Error(`暂无 ${getPlatformName(platform)} 版本下载`)
}
// Find the asset to get architecture info
const asset = release.assets.find((a) => a.browser_download_url === downloadUrl)
const archInfo = asset ? getArchitectureInfo(asset.name) : ''
// Show success notification
showNotification(
`正在下载 ${getPlatformName(platform)} ${archInfo} 版本 ${release.tag_name}...`,
'success'
)
// Use proxy for download if it's a GitHub URL
const finalDownloadUrl = downloadUrl.includes('github.com') ?
`${GITHUB_PROXY}${downloadUrl}` : downloadUrl
// Start download
window.open(finalDownloadUrl, '_blank')
// Track download
trackDownload(platform, release.tag_name, asset ? asset.name : '')
} catch (error) {
console.error('Download error:', error)
showNotification(`下载失败: ${error.message}`, 'error')
// Fallback to GitHub releases page
setTimeout(() => {
showNotification('正在跳转到GitHub下载页面...', 'info')
window.open(`https://github.com/${GITHUB_REPO}/releases/latest`, '_blank')
}, 2000)
} finally {
// Restore button state
setTimeout(() => {
button.innerHTML = originalText
button.disabled = false
}, 1500)
}
}
// Get latest release from GitHub API
async function getLatestRelease() {
// Check cache first
const now = Date.now()
if (releaseData && releaseDataTimestamp && now - releaseDataTimestamp < CACHE_DURATION) {
return releaseData
}
try {
const response = await fetch(GITHUB_API_URL)
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`)
}
const data = await response.json()
// Cache the data
releaseData = data
releaseDataTimestamp = now
return data
} catch (error) {
console.error('Failed to fetch release data:', error)
return null
}
}
// Find appropriate download asset based on platform
function findDownloadAsset(assets, platform) {
if (!assets || !Array.isArray(assets)) {
return null
}
// Filter out unwanted files (yml, yaml, txt, md, etc.)
const filteredAssets = assets.filter((asset) => {
const name = asset.name.toLowerCase()
return (
!name.endsWith('.yml') &&
!name.endsWith('.yaml') &&
!name.endsWith('.txt') &&
!name.endsWith('.md') &&
!name.endsWith('.json') &&
!name.includes('latest') &&
!name.includes('blockmap')
)
})
// Define file patterns for each platform (ordered by priority)
const patterns = {
windows: [
/ceru-music.*win.*x64.*setup\.exe$/i,
/ceru-music.*win.*ia32.*setup\.exe$/i,
/ceru-music.*setup\.exe$/i,
/\.exe$/i,
/ceru-music.*win.*x64.*\.zip$/i,
/ceru-music.*win.*ia32.*\.zip$/i,
/windows.*\.zip$/i,
/win32.*\.zip$/i,
/win.*x64.*\.zip$/i
],
macos: [
/ceru-music.*universal\.dmg$/i,
/ceru-music.*arm64\.dmg$/i,
/ceru-music.*x64\.dmg$/i,
/ceru-music.*\.dmg$/i,
/\.dmg$/i,
/ceru-music.*universal\.zip$/i,
/ceru-music.*arm64\.zip$/i,
/ceru-music.*x64\.zip$/i,
/darwin.*\.zip$/i,
/macos.*\.zip$/i,
/mac.*\.zip$/i,
/osx.*\.zip$/i
],
linux: [
/ceru-music.*linux.*x64\.deb$/i,
/ceru-music.*linux.*x64\.AppImage$/i,
/ceru-music.*amd64\.deb$/i,
/\.deb$/i,
/\.AppImage$/i,
/linux.*\.zip$/i,
/linux.*\.tar\.gz$/i,
/\.rpm$/i
]
}
const platformPatterns = patterns[platform] || []
// Try to find exact match
for (const pattern of platformPatterns) {
const asset = filteredAssets.find((asset) => pattern.test(asset.name))
if (asset) {
return asset.browser_download_url
}
}
// Fallback: look for any asset that might match the platform
const fallbackPatterns = {
windows: /win|exe/i,
macos: /mac|darwin|dmg/i,
linux: /linux|appimage|deb|rpm/i
}
const fallbackPattern = fallbackPatterns[platform]
if (fallbackPattern) {
const asset = filteredAssets.find((asset) => fallbackPattern.test(asset.name))
if (asset) {
return asset.browser_download_url
}
}
return null
}
function getPlatformName(platform) {
const names = {
windows: 'Windows',
macos: 'macOS',
linux: 'Linux'
}
return names[platform] || platform
}
// Notification system
function showNotification(message, type = 'info') {
// Remove existing notifications
const existingNotifications = document.querySelectorAll('.notification')
existingNotifications.forEach((notification) => notification.remove())
// Create notification element
const notification = document.createElement('div')
notification.className = `notification notification-${type}`
notification.innerHTML = `
<div class="notification-content">
<span class="notification-message">${message}</span>
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
`
// Add notification styles if not already added
if (!document.querySelector('#notification-styles')) {
const styles = document.createElement('style')
styles.id = 'notification-styles'
styles.textContent = `
.notification {
position: fixed;
top: 90px;
right: 20px;
background: white;
border: 1px solid var(--border);
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
z-index: 1001;
min-width: 300px;
animation: slideInRight 0.3s ease-out;
}
.notification-info {
border-left: 4px solid var(--primary-color);
}
.notification-success {
border-left: 4px solid #10b981;
}
.notification-error {
border-left: 4px solid #ef4444;
}
.notification-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
}
.notification-message {
color: var(--text-primary);
font-weight: 500;
}
.notification-close {
background: none;
border: none;
cursor: pointer;
color: var(--text-muted);
padding: 0.25rem;
border-radius: 4px;
transition: var(--transition);
}
.notification-close:hover {
background: var(--surface);
color: var(--text-primary);
}
.notification-close svg {
width: 16px;
height: 16px;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@media (max-width: 480px) {
.notification {
right: 10px;
left: 10px;
min-width: auto;
}
}
`
document.head.appendChild(styles)
}
// Add to page
document.body.appendChild(notification)
// Auto remove after 5 seconds
setTimeout(() => {
if (notification.parentElement) {
notification.style.animation = 'slideInRight 0.3s ease-out reverse'
setTimeout(() => notification.remove(), 300)
}
}, 5000)
}
// Navbar scroll effect
function handleNavbarScroll() {
const navbar = document.querySelector('.navbar')
if (window.scrollY > 50) {
navbar.style.background = 'rgba(255, 255, 255, 0.98)'
navbar.style.boxShadow = 'var(--shadow)'
} else {
navbar.style.background = 'rgba(255, 255, 255, 0.95)'
navbar.style.boxShadow = 'none'
}
}
// Intersection Observer for animations
function setupAnimations() {
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.style.animation = 'fadeInUp 0.6s ease-out forwards'
}
})
}, observerOptions)
// Observe feature cards and download cards
document.querySelectorAll('.feature-card, .download-card').forEach((card) => {
card.style.opacity = '0'
card.style.transform = 'translateY(30px)'
observer.observe(card)
})
}
// Auto-detect user's operating system and architecture
function detectOS() {
const userAgent = navigator.userAgent.toLowerCase()
if (userAgent.includes('win')) return 'windows'
if (userAgent.includes('mac')) return 'macos'
if (userAgent.includes('linux')) return 'linux'
return 'windows' // default
}
// Detect user's architecture
function detectArchitecture() {
const userAgent = navigator.userAgent.toLowerCase()
const platform = navigator.platform.toLowerCase()
// For macOS, detect Apple Silicon vs Intel
if (userAgent.includes('mac')) {
// Check for Apple Silicon indicators
if (userAgent.includes('arm') || platform.includes('arm')) {
return 'arm64'
}
// Default to universal for macOS (works on both Intel and Apple Silicon)
return 'universal'
}
// For Windows, detect 32-bit vs 64-bit
if (userAgent.includes('win')) {
if (userAgent.includes('wow64') || userAgent.includes('win64') || userAgent.includes('x64')) {
return 'x64'
}
return 'ia32'
}
// For Linux, assume 64-bit
if (userAgent.includes('linux')) {
return 'x64'
}
return 'x64' // default
}
// Get architecture display name
function getArchitectureName(arch) {
const names = {
x64: '64位',
ia32: '32位',
arm64: 'Apple Silicon',
universal: 'Universal (Intel + Apple Silicon)'
}
return names[arch] || arch
}
// Highlight user's OS download option
function highlightUserOS() {
const userOS = detectOS()
const userArch = detectArchitecture()
const downloadCards = document.querySelectorAll('.download-card')
downloadCards.forEach((card, index) => {
const platforms = ['windows', 'macos', 'linux']
if (platforms[index] === userOS) {
card.style.border = '2px solid var(--primary-color)'
card.style.transform = 'scale(1.02)'
// Add "推荐" badge with architecture info
const badge = document.createElement('div')
badge.className = 'recommended-badge'
const archName = getArchitectureName(userArch)
badge.textContent = `推荐 (${archName})`
badge.style.cssText = `
position: absolute;
top: -10px;
right: 20px;
background: var(--primary-color);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
`
card.style.position = 'relative'
card.appendChild(badge)
// Add architecture info to the card description
const description = card.querySelector('p')
if (description && userOS === 'macos') {
if (userArch === 'arm64') {
description.innerHTML +=
'<br><small style="color: var(--text-muted);">检测到 Apple Silicon Mac推荐 Universal 版本</small>'
} else if (userArch === 'universal') {
description.innerHTML +=
'<br><small style="color: var(--text-muted);">Universal 版本兼容 Intel 和 Apple Silicon Mac</small>'
}
}
}
})
}
// Keyboard navigation
function setupKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
// Close notifications
document.querySelectorAll('.notification').forEach((notification) => {
notification.remove()
})
}
if (e.key === 'Enter' && e.target.classList.contains('btn')) {
e.target.click()
}
})
}
// Performance optimization: Lazy load images
function setupLazyLoading() {
const images = document.querySelectorAll('img[data-src]')
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
img.removeAttribute('data-src')
imageObserver.unobserve(img)
}
})
})
images.forEach((img) => imageObserver.observe(img))
}
// Initialize everything when DOM is loaded
document.addEventListener('DOMContentLoaded', async () => {
// Setup scroll effects
window.addEventListener('scroll', handleNavbarScroll)
// Setup animations
setupAnimations()
// Highlight user's OS
highlightUserOS()
// Setup keyboard navigation
setupKeyboardNavigation()
// Setup lazy loading
setupLazyLoading()
// Add GitHub links
addGitHubLinks()
// Update version information from GitHub
await updateVersionInfo()
// Add smooth scrolling to all anchor links
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener('click', function (e) {
e.preventDefault()
const target = document.querySelector(this.getAttribute('href'))
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
})
})
// Remove the old download button click handlers since downloadApp now handles everything
// The downloadApp function is called directly from the HTML onclick attributes
})
// Add spinning animation for loading state
const spinningStyles = document.createElement('style')
spinningStyles.textContent = `
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`
document.head.appendChild(spinningStyles)
// Error handling for failed downloads
window.addEventListener('error', (e) => {
console.error('页面错误:', e.error)
})
// Update version information on page
async function updateVersionInfo() {
try {
// Get latest release from GitHub
const release = await getLatestRelease()
if (release) {
const versionElement = document.querySelector('.version')
const versionInfoElement = document.querySelector('.version-info p')
if (versionElement) {
versionElement.textContent = release.tag_name
}
if (versionInfoElement) {
const publishDate = new Date(release.published_at)
const formattedDate = publishDate.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
const formattedTime = publishDate.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
versionInfoElement.innerHTML = `当前版本: <span class="version">${release.tag_name}</span> | 更新时间: ${formattedDate} ${formattedTime}`
}
// Update download button text with file sizes if available
updateDownloadButtonsWithAssets(release.assets)
}
} catch (error) {
console.error('Failed to update version info:', error)
}
}
// Find asset for platform (helper function)
function findAssetForPlatform(assets, platform) {
const userArch = detectArchitecture()
// Filter out unwanted files
const filteredAssets = assets.filter((asset) => {
const name = asset.name.toLowerCase()
return (
!name.endsWith('.yml') &&
!name.endsWith('.yaml') &&
!name.endsWith('.txt') &&
!name.endsWith('.md') &&
!name.endsWith('.json') &&
!name.includes('latest') &&
!name.includes('blockmap')
)
})
// Define architecture-specific patterns for each platform
const archPatterns = {
windows: {
x64: [
/ceru-music.*x64.*setup\.exe$/i,
/ceru-music.*win.*x64.*setup\.exe$/i,
/ceru-music.*x64.*\.zip$/i,
/ceru-music.*win.*x64.*\.zip$/i
],
ia32: [
/ceru-music.*ia32.*setup\.exe$/i,
/ceru-music.*win.*ia32.*setup\.exe$/i,
/ceru-music.*ia32.*\.zip$/i,
/ceru-music.*win.*ia32.*\.zip$/i
],
fallback: [/ceru-music.*setup\.exe$/i, /\.exe$/i, /windows.*\.zip$/i, /win.*\.zip$/i]
},
macos: {
universal: [/ceru-music.*universal\.dmg$/i, /ceru-music.*universal\.zip$/i],
arm64: [
/ceru-music.*arm64\.dmg$/i,
/ceru-music.*arm64\.zip$/i,
/ceru-music.*universal\.dmg$/i,
/ceru-music.*universal\.zip$/i
],
x64: [
/ceru-music.*x64\.dmg$/i,
/ceru-music.*x64\.zip$/i,
/ceru-music.*universal\.dmg$/i,
/ceru-music.*universal\.zip$/i
],
fallback: [
/ceru-music.*\.dmg$/i,
/\.dmg$/i,
/darwin.*\.zip$/i,
/macos.*\.zip$/i,
/mac.*\.zip$/i
]
},
linux: {
x64: [
/ceru-music.*linux.*x64\.AppImage$/i,
/ceru-music.*linux.*x64\.deb$/i,
/ceru-music.*x64\.AppImage$/i,
/ceru-music.*x64\.deb$/i
],
fallback: [
/ceru-music.*\.AppImage$/i,
/ceru-music.*\.deb$/i,
/\.AppImage$/i,
/\.deb$/i,
/linux.*\.zip$/i
]
}
}
const platformArchPatterns = archPatterns[platform]
if (!platformArchPatterns) return null
// Try architecture-specific patterns first
const archSpecificPatterns = platformArchPatterns[userArch] || []
for (const pattern of archSpecificPatterns) {
const asset = filteredAssets.find((asset) => pattern.test(asset.name))
if (asset) return asset
}
// Try fallback patterns
const fallbackPatterns = platformArchPatterns.fallback || []
for (const pattern of fallbackPatterns) {
const asset = filteredAssets.find((asset) => pattern.test(asset.name))
if (asset) return asset
}
return null
}
// Update download buttons with asset information
function updateDownloadButtonsWithAssets(assets) {
if (!assets || !Array.isArray(assets)) return
const downloadCards = document.querySelectorAll('.download-card')
const platforms = ['windows', 'macos', 'linux']
downloadCards.forEach((card, index) => {
const platform = platforms[index]
const asset = findAssetForPlatform(assets, platform)
if (asset) {
const button = card.querySelector('.btn-download')
const sizeText = formatFileSize(asset.size)
const originalText = button.innerHTML
// Add file size info
button.innerHTML = originalText.replace(
/下载 \.(.*?)$/,
`下载 .${getFileExtension(asset.name)} (${sizeText})`
)
}
})
}
// Find appropriate download asset based on platform and architecture
function findDownloadAsset(assets, platform, userArch = null) {
if (!assets || !Array.isArray(assets)) {
return null
}
if (!userArch) {
userArch = detectArchitecture()
}
// Filter out unwanted files
const filteredAssets = assets.filter((asset) => {
const name = asset.name.toLowerCase()
return (
!name.endsWith('.yml') &&
!name.endsWith('.yaml') &&
!name.endsWith('.txt') &&
!name.endsWith('.md') &&
!name.endsWith('.json') &&
!name.includes('latest') &&
!name.includes('blockmap')
)
})
// Define architecture-specific patterns for each platform
const archPatterns = {
windows: {
x64: [
/ceru-music.*x64.*setup\.exe$/i,
/ceru-music.*win.*x64.*setup\.exe$/i,
/ceru-music.*x64.*\.zip$/i,
/ceru-music.*win.*x64.*\.zip$/i
],
ia32: [
/ceru-music.*ia32.*setup\.exe$/i,
/ceru-music.*win.*ia32.*setup\.exe$/i,
/ceru-music.*ia32.*\.zip$/i,
/ceru-music.*win.*ia32.*\.zip$/i
],
fallback: [/ceru-music.*setup\.exe$/i, /\.exe$/i, /windows.*\.zip$/i, /win.*\.zip$/i]
},
macos: {
universal: [/ceru-music.*universal\.dmg$/i, /ceru-music.*universal\.zip$/i],
arm64: [
/ceru-music.*arm64\.dmg$/i,
/ceru-music.*arm64\.zip$/i,
/ceru-music.*universal\.dmg$/i,
/ceru-music.*universal\.zip$/i
],
x64: [
/ceru-music.*x64\.dmg$/i,
/ceru-music.*x64\.zip$/i,
/ceru-music.*universal\.dmg$/i,
/ceru-music.*universal\.zip$/i
],
fallback: [
/ceru-music.*\.dmg$/i,
/\.dmg$/i,
/darwin.*\.zip$/i,
/macos.*\.zip$/i,
/mac.*\.zip$/i
]
},
linux: {
x64: [
/ceru-music.*linux.*x64\.AppImage$/i,
/ceru-music.*linux.*x64\.deb$/i,
/ceru-music.*x64\.AppImage$/i,
/ceru-music.*x64\.deb$/i
],
fallback: [
/ceru-music.*\.AppImage$/i,
/ceru-music.*\.deb$/i,
/\.AppImage$/i,
/\.deb$/i,
/linux.*\.zip$/i
]
}
}
const platformArchPatterns = archPatterns[platform]
if (!platformArchPatterns) {
return null
}
// Try architecture-specific patterns first
const archSpecificPatterns = platformArchPatterns[userArch] || []
for (const pattern of archSpecificPatterns) {
const asset = filteredAssets.find((asset) => pattern.test(asset.name))
if (asset) return asset.browser_download_url
}
// Try fallback patterns
const fallbackPatterns = platformArchPatterns.fallback || []
for (const pattern of fallbackPatterns) {
const asset = filteredAssets.find((asset) => pattern.test(asset.name))
if (asset) return asset.browser_download_url
}
// Final fallback: look for any asset that might match the platform
const finalFallbackPatterns = {
windows: /win|exe/i,
macos: /mac|darwin|dmg/i,
linux: /linux|appimage|deb|rpm/i
}
const finalPattern = finalFallbackPatterns[platform]
if (finalPattern) {
const asset = filteredAssets.find((asset) => finalPattern.test(asset.name))
if (asset) return asset.browser_download_url
}
return null
}
// Helper function to get file extension
function getFileExtension(filename) {
return filename.split('.').pop()
}
// Helper function to format file size
function formatFileSize(bytes) {
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]
}
// Get architecture information from filename
function getArchitectureInfo(filename) {
if (!filename) return ''
const name = filename.toLowerCase()
if (name.includes('universal')) return '(Universal)'
if (name.includes('arm64')) return '(Apple Silicon)'
if (name.includes('x64')) return '(64位)'
if (name.includes('ia32')) return '(32位)'
if (name.includes('win') && name.includes('x64')) return '(64位)'
if (name.includes('win') && name.includes('ia32')) return '(32位)'
if (name.includes('linux') && name.includes('x64')) return '(64位)'
return ''
}
// Analytics tracking (placeholder)
function trackDownload(platform, version, filename = '') {
// Add your analytics tracking code here
const archInfo = getArchitectureInfo(filename)
// Example: Google Analytics
// gtag('event', 'download', {
// 'event_category': 'software',
// 'event_label': `${platform}_${archInfo}`,
// 'value': version
// });
}
// Version comparison function to handle complex version numbers like v1.3.10, v1.3.3.1
function compareVersions(a, b) {
// Remove 'v' prefix if present
const versionA = a.replace(/^v/, '')
const versionB = b.replace(/^v/, '')
// Split version numbers into parts
const partsA = versionA.split('.').map(num => parseInt(num, 10))
const partsB = versionB.split('.').map(num => parseInt(num, 10))
// Compare each part
const maxLength = Math.max(partsA.length, partsB.length)
for (let i = 0; i < maxLength; i++) {
const partA = partsA[i] || 0
const partB = partsB[i] || 0
if (partA > partB) return 1
if (partA < partB) return -1
}
return 0
}
// Add GitHub link functionality
function addGitHubLinks() {
// Add GitHub link to footer if not exists
const footerSection = document.querySelector('.footer-section:nth-child(3) ul')
if (footerSection) {
const githubLink = document.createElement('li')
githubLink.innerHTML = `<a href="https://github.com/${GITHUB_REPO}" target="_blank">GitHub 仓库</a>`
footerSection.appendChild(githubLink)
}
// Add "查看所有版本" link to download section
const versionInfo = document.querySelector('.version-info')
if (versionInfo) {
const allVersionsLink = document.createElement('p')
allVersionsLink.innerHTML = `<a href="https://github.com/${GITHUB_REPO}/releases" target="_blank" style="color: var(--primary-color); text-decoration: none;">查看所有版本 →</a>`
allVersionsLink.style.marginTop = '1rem'
versionInfo.appendChild(allVersionsLink)
}
}