Reachy_Mini / src /components /InstallModal.jsx
tfrere's picture
tfrere HF Staff
feat(download): add Linux beta support
a540fdf
import { useMemo } from 'react';
import {
Avatar,
Box,
Button,
Chip,
Dialog,
DialogContent,
IconButton,
Link,
Typography,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import VerifiedIcon from '@mui/icons-material/Verified';
import DownloadIcon from '@mui/icons-material/Download';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import ComputerIcon from '@mui/icons-material/Computer';
function InstallModal({ open, onClose, app }) {
// Detect Linux users
const isLinux = useMemo(() => {
if (typeof navigator === 'undefined') return false;
const platform = navigator.platform?.toLowerCase() || '';
const userAgent = navigator.userAgent?.toLowerCase() || '';
return platform.includes('linux') || userAgent.includes('linux');
}, []);
if (!app) return null;
const appName = app.name || app.id?.split('/').pop();
const cardData = app.cardData || {};
const emoji = cardData.emoji || '📦';
const description = cardData.short_description || app.description || 'No description';
const deepLinkUrl = `reachymini://install/${appName}`;
const spaceUrl = `https://huggingface.co/spaces/${app.id}`;
const author = app.id?.split('/')?.[0] || app.author || null;
const isOfficial = app.isOfficial;
const likes = app.likes || 0;
const lastModified = app.lastModified || app.createdAt || null;
const formattedDate = lastModified
? new Date(lastModified).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
: null;
const handleInstall = () => {
window.location.href = deepLinkUrl;
setTimeout(() => onClose(), 500);
};
return (
<Dialog
open={open}
onClose={onClose}
PaperProps={{
sx: {
borderRadius: '20px',
maxWidth: 520,
minWidth: { xs: 'auto', sm: 480 },
width: '100%',
mx: 2,
overflow: 'visible',
bgcolor: '#fff',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
},
}}
>
{/* Close button outside modal */}
<IconButton
onClick={onClose}
disableRipple
sx={{
position: 'absolute',
top: 0,
right: -44,
color: 'rgba(255,255,255,0.8)',
p: 0.5,
'&:hover': { color: '#fff', bgcolor: 'transparent' },
}}
>
<CloseIcon sx={{ fontSize: 24 }} />
</IconButton>
<DialogContent sx={{ p: 0, overflow: 'hidden', borderRadius: '20px' }}>
{/* Header */}
<Box sx={{ p: 3 }}>
{/* App row */}
<Box sx={{ display: 'flex', gap: 2.5 }}>
{/* Emoji */}
<Box
sx={{
width: 72,
height: 72,
borderRadius: '18px',
bgcolor: '#f5f5f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 36,
flexShrink: 0,
}}
>
{emoji}
</Box>
{/* Info */}
<Box sx={{ flex: 1, minWidth: 0, pr: 4 }}>
{/* Title */}
<Typography
sx={{
fontSize: 20,
fontWeight: 700,
color: '#1a1a1a',
mb: 1,
lineHeight: 1.2,
}}
>
{appName}
</Typography>
{/* Meta row */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
{author && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Avatar
sx={{
width: 20,
height: 20,
bgcolor: '#e0e0e0',
fontSize: 10,
fontWeight: 600,
color: '#666',
}}
>
{author.charAt(0).toUpperCase()}
</Avatar>
<Typography sx={{ fontSize: 13, color: '#666', fontFamily: 'monospace' }}>
{author}
</Typography>
</Box>
)}
{isOfficial && (
<Chip
icon={<VerifiedIcon sx={{ fontSize: 14 }} />}
label="Official"
size="small"
sx={{
height: 24,
bgcolor: 'rgba(255, 149, 0, 0.1)',
color: '#FF9500',
fontWeight: 600,
fontSize: 11,
'& .MuiChip-icon': { color: '#FF9500', ml: 0.5 },
'& .MuiChip-label': { px: 1 },
}}
/>
)}
</Box>
{/* Stats row */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<FavoriteBorderIcon sx={{ fontSize: 14, color: '#999' }} />
<Typography sx={{ fontSize: 12, color: '#999' }}>{likes}</Typography>
</Box>
{formattedDate && (
<Typography sx={{ fontSize: 12, color: '#aaa' }}>
Updated {formattedDate}
</Typography>
)}
</Box>
{/* Description - intégrée au bloc info */}
<Typography
sx={{
fontSize: 13.5,
color: '#666',
lineHeight: 1.6,
mt: 1.5,
}}
>
{description}
</Typography>
</Box>
</Box>
</Box>
{/* Divider */}
<Box sx={{ height: 1, bgcolor: '#f0f0f0', mx: 3 }} />
{/* Desktop App Requirement Block */}
<Box sx={{ p: 3 }}>
{/* All platforms: Normal install flow */}
<Box
sx={{
display: 'flex',
alignItems: 'flex-start',
gap: 2,
p: 2.5,
borderRadius: '14px',
bgcolor: 'rgba(255, 149, 0, 0.05)',
border: '1px solid rgba(255, 149, 0, 0.12)',
}}
>
<Box
sx={{
width: 44,
height: 44,
borderRadius: '12px',
bgcolor: 'rgba(255, 149, 0, 0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<ComputerIcon sx={{ fontSize: 24, color: '#FF9500' }} />
</Box>
<Box sx={{ flex: 1 }}>
<Typography
sx={{
fontSize: 14,
fontWeight: 700,
color: '#1a1a1a',
mb: 0.75,
}}
>
Reachy Mini Desktop App required
</Typography>
<Typography
sx={{
fontSize: 13,
color: '#666',
lineHeight: 1.6,
mb: 1.5,
}}
>
No robot? <Link
href="#"
sx={{
color: '#FF9500',
fontWeight: 600,
textDecoration: 'none',
'&:hover': { textDecoration: 'underline' },
}}
>
Try it in simulation mode
</Link> – no hardware needed!
</Typography>
<Link
href="/download"
sx={{
display: 'inline-block',
fontSize: 13,
fontWeight: 600,
color: '#FF9500',
textDecoration: 'none',
'&:hover': { textDecoration: 'underline' },
}}
>
Download the desktop app →
</Link>
</Box>
</Box>
{/* Linux Beta Notice */}
{isLinux && (
<Box
sx={{
mt: 2,
p: 1.5,
borderRadius: '10px',
bgcolor: 'rgba(59, 130, 246, 0.08)',
border: '1px solid rgba(59, 130, 246, 0.2)',
}}
>
<Typography
sx={{
fontSize: 12,
color: '#3b82f6',
fontWeight: 500,
textAlign: 'center',
'& a': {
color: '#3b82f6',
textDecoration: 'underline',
fontWeight: 600,
},
}}
>
Linux support is currently in Beta — please report any issues on{' '}
<a href="https://github.com/pollen-robotics/reachy-mini-desktop-app/issues" target="_blank" rel="noopener noreferrer">GitHub</a>
{' '}or{' '}
<a href="https://discord.gg/HDrGY9eJHt" target="_blank" rel="noopener noreferrer">Discord</a>.
</Typography>
</Box>
)}
</Box>
{/* Actions */}
<Box sx={{ px: 3, pb: 3, display: 'flex', gap: 2 }}>
<Button
component={Link}
href={spaceUrl}
target="_blank"
rel="noopener noreferrer"
variant="outlined"
sx={{
flex: 1,
py: 1.5,
borderRadius: '12px',
borderColor: '#ddd',
color: '#555',
fontWeight: 600,
fontSize: 14,
textTransform: 'none',
textDecoration: 'none',
gap: 0.75,
'&:hover': { borderColor: '#bbb', bgcolor: '#fafafa' },
}}
>
App Page
<OpenInNewIcon sx={{ fontSize: 16 }} />
</Button>
<Button
variant="contained"
onClick={handleInstall}
startIcon={<DownloadIcon sx={{ fontSize: 20 }} />}
sx={{
flex: 1.5,
py: 1.5,
borderRadius: '12px',
bgcolor: '#FF9500',
fontWeight: 600,
fontSize: 14,
textTransform: 'none',
boxShadow: 'none',
'&:hover': {
bgcolor: '#e68600',
boxShadow: '0 4px 12px rgba(255, 149, 0, 0.35)',
},
}}
>
Install
</Button>
</Box>
</DialogContent>
</Dialog>
);
}
export default InstallModal;