import { useState, useEffect } from 'react'; import { Box, Container, Typography, Button, Grid, Card, CardContent, Chip, Stack, CircularProgress, } from '@mui/material'; import DownloadIcon from '@mui/icons-material/Download'; import AppleIcon from '@mui/icons-material/Apple'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import Layout from '../components/Layout'; // Platform configuration const PLATFORMS = { 'darwin-aarch64': { name: 'macOS', subtitle: 'Apple Silicon', arch: 'M1, M2, M3, M4', format: '.dmg', icon: AppleIcon, color: '#a3a3a3', }, 'darwin-x86_64': { name: 'macOS', subtitle: 'Intel', arch: 'x86_64', format: '.dmg', icon: AppleIcon, color: '#a3a3a3', }, 'windows-x86_64': { name: 'Windows', subtitle: '64-bit', arch: 'x86_64', format: '.msi', icon: null, color: '#0078d4', }, 'linux-x86_64': { name: 'Linux', subtitle: 'Debian/Ubuntu', arch: 'x86_64', format: '.deb', icon: null, color: '#e95420', }, }; // URL to fetch latest release info (using GitHub API for CORS support) const GITHUB_RELEASES_API = 'https://api.github.com/repos/pollen-robotics/reachy-mini-desktop-app/releases/latest'; const GITHUB_RELEASES_LIST_API = 'https://api.github.com/repos/pollen-robotics/reachy-mini-desktop-app/releases?per_page=10'; // Detect user's platform function detectPlatform() { const ua = navigator.userAgent; const platform = navigator.platform || ''; if (/Mac/.test(platform) || /Mac/.test(ua)) { return 'darwin-aarch64'; } if (/Win/.test(platform) || /Windows/.test(ua)) { return 'windows-x86_64'; } if (/Linux/.test(platform) || /Linux/.test(ua)) { return 'linux-x86_64'; } return 'darwin-aarch64'; } // Format date function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', }); } // Parse release body and extract clean changes function parseReleaseChanges(body) { if (!body) return []; const changes = []; const lines = body.split('\n'); for (const line of lines) { const trimmed = line.trim(); // Skip empty lines, headers, and meta content if (!trimmed) continue; if (trimmed.startsWith('##')) continue; // Skip all headers (## What's Changed, etc.) if (trimmed.startsWith('**Full Changelog**')) continue; if (trimmed.startsWith('**New Contributors**')) continue; if (trimmed.includes('made their first contribution')) continue; if (trimmed.startsWith('')) continue; if (trimmed === 'See the assets to download this version and install.') continue; // Parse change lines (starting with * or -) if (trimmed.startsWith('*') || trimmed.startsWith('-')) { let change = trimmed.replace(/^[\*\-]\s*/, ''); // Extract the description from markdown links: "fix: description by @user in https://..." // We want to keep: "fix: description" const byMatch = change.match(/^(.+?)\s+by\s+@\w+/i); if (byMatch) { change = byMatch[1].trim(); } // Remove trailing "in https://..." links change = change.replace(/\s+in\s+https:\/\/[^\s]+$/i, ''); // Clean up any remaining markdown link syntax [text](url) -> text change = change.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); // Skip if it's just a contributor line or empty after cleaning if (change && change.length > 3 && !change.includes('first contribution')) { changes.push(change); } } } return changes; } // Windows Icon function WindowsIcon({ sx = {} }) { return ( ); } // Linux Icon function LinuxIcon({ sx = {} }) { return ( ); } // Platform Card component function PlatformCard({ platformKey, url, isActive, onClick }) { const platform = PLATFORMS[platformKey]; const Icon = platform?.icon; const isComingSoon = platformKey.includes('windows') || platformKey.includes('linux'); return ( {/* Coming soon tag */} {isComingSoon && ( )} {Icon ? ( ) : platformKey.includes('windows') ? ( ) : ( )} {platform?.name} {platform?.subtitle} ); } export default function Download() { const [releaseData, setReleaseData] = useState(null); const [allReleases, setAllReleases] = useState([]); const [detectedPlatform, setDetectedPlatform] = useState(null); const [loading, setLoading] = useState(true); const [showAllReleases, setShowAllReleases] = useState(false); const [error, setError] = useState(null); useEffect(() => { setDetectedPlatform(detectPlatform()); // Fetch latest release info from GitHub API async function fetchReleases() { try { // Fetch latest release for download buttons const latestResponse = await fetch(GITHUB_RELEASES_API); // Fetch all releases for changelog const allResponse = await fetch(GITHUB_RELEASES_LIST_API); if (latestResponse.ok) { const data = await latestResponse.json(); // Transform GitHub API response to our format const version = data.tag_name?.replace('v', '') || ''; const platforms = {}; // Map assets to platforms (prioritize user-friendly formats) data.assets?.forEach(asset => { const name = asset.name.toLowerCase(); const url = asset.browser_download_url; // macOS Apple Silicon - prefer .dmg if (name.includes('arm64.dmg')) { platforms['darwin-aarch64'] = { url }; } else if (name.includes('darwin-aarch64') && !platforms['darwin-aarch64']) { platforms['darwin-aarch64'] = { url }; } // macOS Intel - prefer .dmg if (name.includes('x64.dmg') && !name.includes('arm64')) { platforms['darwin-x86_64'] = { url }; } else if (name.includes('darwin-x86_64') && !platforms['darwin-x86_64']) { platforms['darwin-x86_64'] = { url }; } // Windows - .msi if (name.includes('.msi')) { platforms['windows-x86_64'] = { url }; } // Linux - .deb if (name.includes('amd64.deb')) { platforms['linux-x86_64'] = { url }; } }); setReleaseData({ version, pub_date: data.published_at, platforms, }); } else { setError('Failed to fetch release info'); } // Set all releases for changelog if (allResponse.ok) { const releases = await allResponse.json(); setAllReleases(releases.filter(r => !r.draft)); } } catch (err) { console.error('Error fetching release:', err); setError('Failed to fetch release info'); } finally { setLoading(false); } } fetchReleases(); }, []); if (loading) { return ( ); } if (error || !releaseData) { return ( Unable to load release info ); } const currentPlatform = PLATFORMS[detectedPlatform]; const currentUrl = releaseData?.platforms[detectedPlatform]?.url; return ( {/* Subtle gradient orbs - spread across the page */} {/* Hero Section */} {/* App icon */} Reachy Mini Control The official desktop app to control, program, and play with your Reachy Mini. {/* Version info */} } label={`v${releaseData?.version}`} sx={{ backgroundColor: 'rgba(16, 185, 129, 0.1)', color: '#10b981', fontWeight: 600, border: '1px solid rgba(16, 185, 129, 0.2)', }} /> Released {formatDate(releaseData?.pub_date)} {/* Primary download button - different for macOS vs Windows/Linux */} {detectedPlatform?.startsWith('darwin') ? ( <> {currentPlatform?.subtitle} • {currentPlatform?.format?.replace('.', '').toUpperCase()} package ) : ( <> {/* Windows/Linux - Coming soon message */} 🚧 {currentPlatform?.name} support coming soon! We're working hard to bring Reachy Mini Control to {currentPlatform?.name}. In the meantime, macOS is fully supported! )} {/* App screenshot */} {/* All platforms */} Available for all platforms {['darwin-aarch64', 'darwin-x86_64', 'windows-x86_64', 'linux-x86_64'].map((key) => ( setDetectedPlatform(key)} /> ))} {/* Platform support notice - only show on macOS */} {detectedPlatform?.startsWith('darwin') && ( 🚧 Windows & Linux support coming soon )} {/* Features / What's included */} What's included {[ '3D visualization of your robot', 'Real-time motor control', 'App Store with 30+ apps', 'Camera & microphone access', 'Record & playback movements', 'Full SDK integration', ].map((feature, i) => ( {feature} ))} {/* Requirements */} Requires macOS 11+, Windows 10+, or Debian/Ubuntu Linux {/* Release Notes */} {allReleases.length > 0 && ( Release Notes {(showAllReleases ? allReleases : allReleases.slice(0, 5)) .map((release) => { const changes = parseReleaseChanges(release.body); return ( 0 ? 1 : 0 }}> {release.tag_name} {release.prerelease && ( )} {changes.length > 0 && ( {changes.map((change, i) => ( {change} ))} )} ); })} {allReleases.length > 5 && ( )} )} ); }