tfrere HF Staff commited on
Commit
14e18f1
·
1 Parent(s): d355989

feat: add InstallModal for app installation via deep links

Browse files
src/assets/reachy-mini-access-point-QR-code.png ADDED

Git LFS Details

  • SHA256: deb7d1a6526187f7513a446e062c7fbd5312943efdf6556aee468fa83d627350
  • Pointer size: 129 Bytes
  • Size of remote file: 1.87 kB
src/components/InstallModal.jsx ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo } from 'react';
2
+ import {
3
+ Avatar,
4
+ Box,
5
+ Button,
6
+ Chip,
7
+ Dialog,
8
+ DialogContent,
9
+ IconButton,
10
+ Link,
11
+ Typography,
12
+ } from '@mui/material';
13
+ import CloseIcon from '@mui/icons-material/Close';
14
+ import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
15
+ import VerifiedIcon from '@mui/icons-material/Verified';
16
+ import DownloadIcon from '@mui/icons-material/Download';
17
+ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
18
+ import ComputerIcon from '@mui/icons-material/Computer';
19
+ import BlockIcon from '@mui/icons-material/Block';
20
+
21
+ function InstallModal({ open, onClose, app }) {
22
+ // Detect Linux users
23
+ const isLinux = useMemo(() => {
24
+ if (typeof navigator === 'undefined') return false;
25
+ const platform = navigator.platform?.toLowerCase() || '';
26
+ const userAgent = navigator.userAgent?.toLowerCase() || '';
27
+ return platform.includes('linux') || userAgent.includes('linux');
28
+ }, []);
29
+
30
+ if (!app) return null;
31
+
32
+ const appName = app.name || app.id?.split('/').pop();
33
+ const cardData = app.cardData || {};
34
+ const emoji = cardData.emoji || '📦';
35
+ const description = cardData.short_description || app.description || 'No description';
36
+ const deepLinkUrl = `reachymini://install/${appName}`;
37
+ const spaceUrl = `https://huggingface.co/spaces/${app.id}`;
38
+
39
+ const author = app.id?.split('/')?.[0] || app.author || null;
40
+ const isOfficial = app.isOfficial;
41
+ const likes = app.likes || 0;
42
+ const lastModified = app.lastModified || app.createdAt || null;
43
+ const formattedDate = lastModified
44
+ ? new Date(lastModified).toLocaleDateString('en-US', {
45
+ month: 'short',
46
+ day: 'numeric',
47
+ year: 'numeric',
48
+ })
49
+ : null;
50
+
51
+ const handleInstall = () => {
52
+ window.location.href = deepLinkUrl;
53
+ setTimeout(() => onClose(), 500);
54
+ };
55
+
56
+ return (
57
+ <Dialog
58
+ open={open}
59
+ onClose={onClose}
60
+ PaperProps={{
61
+ sx: {
62
+ borderRadius: '20px',
63
+ maxWidth: 440,
64
+ width: '100%',
65
+ mx: 2,
66
+ overflow: 'visible',
67
+ bgcolor: '#fff',
68
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
69
+ },
70
+ }}
71
+ >
72
+ {/* Close button outside modal */}
73
+ <IconButton
74
+ onClick={onClose}
75
+ disableRipple
76
+ sx={{
77
+ position: 'absolute',
78
+ top: 0,
79
+ right: -44,
80
+ color: 'rgba(255,255,255,0.8)',
81
+ p: 0.5,
82
+ '&:hover': { color: '#fff', bgcolor: 'transparent' },
83
+ }}
84
+ >
85
+ <CloseIcon sx={{ fontSize: 24 }} />
86
+ </IconButton>
87
+
88
+ <DialogContent sx={{ p: 0, overflow: 'hidden', borderRadius: '20px' }}>
89
+ {/* Header */}
90
+ <Box sx={{ p: 3 }}>
91
+
92
+ {/* App row */}
93
+ <Box sx={{ display: 'flex', gap: 2.5 }}>
94
+ {/* Emoji */}
95
+ <Box
96
+ sx={{
97
+ width: 72,
98
+ height: 72,
99
+ borderRadius: '18px',
100
+ bgcolor: '#f5f5f5',
101
+ display: 'flex',
102
+ alignItems: 'center',
103
+ justifyContent: 'center',
104
+ fontSize: 36,
105
+ flexShrink: 0,
106
+ }}
107
+ >
108
+ {emoji}
109
+ </Box>
110
+
111
+ {/* Info */}
112
+ <Box sx={{ flex: 1, minWidth: 0, pr: 4 }}>
113
+ {/* Title */}
114
+ <Typography
115
+ sx={{
116
+ fontSize: 20,
117
+ fontWeight: 700,
118
+ color: '#1a1a1a',
119
+ mb: 1,
120
+ lineHeight: 1.2,
121
+ }}
122
+ >
123
+ {appName}
124
+ </Typography>
125
+
126
+ {/* Meta row */}
127
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
128
+ {author && (
129
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
130
+ <Avatar
131
+ sx={{
132
+ width: 20,
133
+ height: 20,
134
+ bgcolor: '#e0e0e0',
135
+ fontSize: 10,
136
+ fontWeight: 600,
137
+ color: '#666',
138
+ }}
139
+ >
140
+ {author.charAt(0).toUpperCase()}
141
+ </Avatar>
142
+ <Typography sx={{ fontSize: 13, color: '#666', fontFamily: 'monospace' }}>
143
+ {author}
144
+ </Typography>
145
+ </Box>
146
+ )}
147
+ {isOfficial && (
148
+ <Chip
149
+ icon={<VerifiedIcon sx={{ fontSize: 14 }} />}
150
+ label="Official"
151
+ size="small"
152
+ sx={{
153
+ height: 24,
154
+ bgcolor: 'rgba(255, 149, 0, 0.1)',
155
+ color: '#FF9500',
156
+ fontWeight: 600,
157
+ fontSize: 11,
158
+ '& .MuiChip-icon': { color: '#FF9500', ml: 0.5 },
159
+ '& .MuiChip-label': { px: 1 },
160
+ }}
161
+ />
162
+ )}
163
+ </Box>
164
+
165
+ {/* Stats row */}
166
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
167
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
168
+ <FavoriteBorderIcon sx={{ fontSize: 14, color: '#999' }} />
169
+ <Typography sx={{ fontSize: 12, color: '#999' }}>{likes}</Typography>
170
+ </Box>
171
+ {formattedDate && (
172
+ <Typography sx={{ fontSize: 12, color: '#aaa' }}>
173
+ Updated {formattedDate}
174
+ </Typography>
175
+ )}
176
+ </Box>
177
+
178
+ {/* Description - intégrée au bloc info */}
179
+ <Typography
180
+ sx={{
181
+ fontSize: 13.5,
182
+ color: '#666',
183
+ lineHeight: 1.6,
184
+ mt: 1.5,
185
+ }}
186
+ >
187
+ {description}
188
+ </Typography>
189
+ </Box>
190
+ </Box>
191
+ </Box>
192
+
193
+ {/* Divider */}
194
+ <Box sx={{ height: 1, bgcolor: '#f0f0f0', mx: 3 }} />
195
+
196
+ {/* Desktop App Requirement Block */}
197
+ <Box sx={{ p: 3 }}>
198
+ {isLinux ? (
199
+ // Linux: Not supported message
200
+ <Box
201
+ sx={{
202
+ display: 'flex',
203
+ alignItems: 'flex-start',
204
+ gap: 2,
205
+ p: 2.5,
206
+ borderRadius: '14px',
207
+ bgcolor: 'rgba(0, 0, 0, 0.03)',
208
+ border: '1px solid rgba(0, 0, 0, 0.08)',
209
+ }}
210
+ >
211
+ <Box
212
+ sx={{
213
+ width: 44,
214
+ height: 44,
215
+ borderRadius: '12px',
216
+ bgcolor: 'rgba(0, 0, 0, 0.05)',
217
+ display: 'flex',
218
+ alignItems: 'center',
219
+ justifyContent: 'center',
220
+ flexShrink: 0,
221
+ }}
222
+ >
223
+ <BlockIcon sx={{ fontSize: 24, color: '#999' }} />
224
+ </Box>
225
+ <Box sx={{ flex: 1 }}>
226
+ <Typography
227
+ sx={{
228
+ fontSize: 14,
229
+ fontWeight: 700,
230
+ color: '#1a1a1a',
231
+ mb: 0.75,
232
+ }}
233
+ >
234
+ Linux not supported yet
235
+ </Typography>
236
+ <Typography
237
+ sx={{
238
+ fontSize: 13,
239
+ color: '#666',
240
+ lineHeight: 1.6,
241
+ }}
242
+ >
243
+ The desktop app is currently available for <strong>macOS</strong> and <strong>Windows</strong> only.
244
+ Linux support is coming soon!
245
+ </Typography>
246
+ </Box>
247
+ </Box>
248
+ ) : (
249
+ // macOS/Windows: Normal install flow
250
+ <Box
251
+ sx={{
252
+ display: 'flex',
253
+ alignItems: 'flex-start',
254
+ gap: 2,
255
+ p: 2.5,
256
+ borderRadius: '14px',
257
+ bgcolor: 'rgba(255, 149, 0, 0.05)',
258
+ border: '1px solid rgba(255, 149, 0, 0.12)',
259
+ }}
260
+ >
261
+ <Box
262
+ sx={{
263
+ width: 44,
264
+ height: 44,
265
+ borderRadius: '12px',
266
+ bgcolor: 'rgba(255, 149, 0, 0.1)',
267
+ display: 'flex',
268
+ alignItems: 'center',
269
+ justifyContent: 'center',
270
+ flexShrink: 0,
271
+ }}
272
+ >
273
+ <ComputerIcon sx={{ fontSize: 24, color: '#FF9500' }} />
274
+ </Box>
275
+ <Box sx={{ flex: 1 }}>
276
+ <Typography
277
+ sx={{
278
+ fontSize: 14,
279
+ fontWeight: 700,
280
+ color: '#1a1a1a',
281
+ mb: 0.75,
282
+ }}
283
+ >
284
+ Reachy Mini Desktop App required
285
+ </Typography>
286
+ <Typography
287
+ sx={{
288
+ fontSize: 13,
289
+ color: '#666',
290
+ lineHeight: 1.6,
291
+ mb: 1.5,
292
+ }}
293
+ >
294
+ No robot? <Link
295
+ href="#"
296
+ sx={{
297
+ color: '#FF9500',
298
+ fontWeight: 600,
299
+ textDecoration: 'none',
300
+ '&:hover': { textDecoration: 'underline' },
301
+ }}
302
+ >
303
+ Try it in simulation mode
304
+ </Link> – no hardware needed!
305
+ </Typography>
306
+ <Link
307
+ href="/download"
308
+ sx={{
309
+ display: 'inline-block',
310
+ fontSize: 13,
311
+ fontWeight: 600,
312
+ color: '#FF9500',
313
+ textDecoration: 'none',
314
+ '&:hover': { textDecoration: 'underline' },
315
+ }}
316
+ >
317
+ Download the desktop app →
318
+ </Link>
319
+ </Box>
320
+ </Box>
321
+ )}
322
+ </Box>
323
+
324
+ {/* Actions */}
325
+ <Box sx={{ px: 3, pb: 3, display: 'flex', gap: 2 }}>
326
+ <Button
327
+ component={Link}
328
+ href={spaceUrl}
329
+ target="_blank"
330
+ rel="noopener noreferrer"
331
+ variant="outlined"
332
+ sx={{
333
+ flex: isLinux ? 1 : 1,
334
+ py: 1.5,
335
+ borderRadius: '12px',
336
+ borderColor: '#ddd',
337
+ color: '#555',
338
+ fontWeight: 600,
339
+ fontSize: 14,
340
+ textTransform: 'none',
341
+ textDecoration: 'none',
342
+ gap: 0.75,
343
+ '&:hover': { borderColor: '#bbb', bgcolor: '#fafafa' },
344
+ }}
345
+ >
346
+ App Page
347
+ <OpenInNewIcon sx={{ fontSize: 16 }} />
348
+ </Button>
349
+
350
+ {!isLinux && (
351
+ <Button
352
+ variant="contained"
353
+ onClick={handleInstall}
354
+ startIcon={<DownloadIcon sx={{ fontSize: 20 }} />}
355
+ sx={{
356
+ flex: 1.5,
357
+ py: 1.5,
358
+ borderRadius: '12px',
359
+ bgcolor: '#FF9500',
360
+ fontWeight: 600,
361
+ fontSize: 14,
362
+ textTransform: 'none',
363
+ boxShadow: 'none',
364
+ '&:hover': {
365
+ bgcolor: '#e68600',
366
+ boxShadow: '0 4px 12px rgba(255, 149, 0, 0.35)',
367
+ },
368
+ }}
369
+ >
370
+ Install
371
+ </Button>
372
+ )}
373
+ </Box>
374
+ </DialogContent>
375
+ </Dialog>
376
+ );
377
+ }
378
+
379
+ export default InstallModal;
src/pages/Apps.jsx CHANGED
@@ -11,19 +11,21 @@ import {
11
  CircularProgress,
12
  Link,
13
  IconButton,
 
14
  } from '@mui/material';
15
  import SearchIcon from '@mui/icons-material/Search';
16
  import CloseIcon from '@mui/icons-material/Close';
17
  import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
18
  import AccessTimeIcon from '@mui/icons-material/AccessTime';
19
- import OpenInNewIcon from '@mui/icons-material/OpenInNew';
20
  import VerifiedIcon from '@mui/icons-material/Verified';
 
21
  import Layout from '../components/Layout';
22
  import ReachiesCarousel from '../components/ReachiesCarousel';
23
  import { useApps } from '../context/AppsContext';
 
24
 
25
  // App Card Component
26
- function AppCard({ app }) {
27
  const isOfficial = app.isOfficial;
28
  const cardData = app.cardData || {};
29
  const author = app.id?.split('/')?.[0] || app.author || null;
@@ -35,14 +37,14 @@ function AppCard({ app }) {
35
  ? new Date(lastModified).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
36
  : null;
37
 
38
- const spaceUrl = `https://huggingface.co/spaces/${app.id}`;
 
 
 
 
39
 
40
  return (
41
  <Box
42
- component={Link}
43
- href={spaceUrl}
44
- target="_blank"
45
- rel="noopener noreferrer"
46
  sx={{
47
  display: 'flex',
48
  flexDirection: 'column',
@@ -51,9 +53,7 @@ function AppCard({ app }) {
51
  overflow: 'hidden',
52
  bgcolor: '#ffffff',
53
  border: isOfficial ? '1px solid rgba(59, 130, 246, 0.25)' : '1px solid rgba(0, 0, 0, 0.08)',
54
- cursor: 'pointer',
55
  transition: 'all 0.2s ease',
56
- textDecoration: 'none',
57
  '&:hover': {
58
  transform: 'translateY(-4px)',
59
  boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
@@ -203,9 +203,10 @@ function AppCard({ app }) {
203
  {cardData.short_description || app.description || 'No description'}
204
  </Typography>
205
 
206
- {/* Date + Open Link */}
207
  <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
208
- {formattedDate && (
 
209
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
210
  <AccessTimeIcon sx={{ fontSize: 14, color: '#999' }} />
211
  <Typography
@@ -218,14 +219,31 @@ function AppCard({ app }) {
218
  {formattedDate}
219
  </Typography>
220
  </Box>
 
 
221
  )}
222
 
223
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: '#FF9500' }}>
224
- <Typography sx={{ fontSize: 12, fontWeight: 600 }}>
225
- View on HF
226
- </Typography>
227
- <OpenInNewIcon sx={{ fontSize: 14 }} />
228
- </Box>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  </Box>
230
  </Box>
231
  </Box>
@@ -238,6 +256,20 @@ export default function Apps() {
238
  const { apps, loading, error } = useApps();
239
  const [searchQuery, setSearchQuery] = useState('');
240
  const [officialOnly, setOfficialOnly] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
  // Filter apps based on search and official toggle
243
  const filteredApps = useMemo(() => {
@@ -509,11 +541,19 @@ export default function Apps() {
509
  <AppCard
510
  key={app.id || index}
511
  app={app}
 
512
  />
513
  ))}
514
  </Box>
515
  )}
516
  </Container>
 
 
 
 
 
 
 
517
  </Layout>
518
  );
519
  }
 
11
  CircularProgress,
12
  Link,
13
  IconButton,
14
+ Button,
15
  } from '@mui/material';
16
  import SearchIcon from '@mui/icons-material/Search';
17
  import CloseIcon from '@mui/icons-material/Close';
18
  import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
19
  import AccessTimeIcon from '@mui/icons-material/AccessTime';
 
20
  import VerifiedIcon from '@mui/icons-material/Verified';
21
+ import DownloadIcon from '@mui/icons-material/Download';
22
  import Layout from '../components/Layout';
23
  import ReachiesCarousel from '../components/ReachiesCarousel';
24
  import { useApps } from '../context/AppsContext';
25
+ import InstallModal from '../components/InstallModal';
26
 
27
  // App Card Component
28
+ function AppCard({ app, onInstallClick }) {
29
  const isOfficial = app.isOfficial;
30
  const cardData = app.cardData || {};
31
  const author = app.id?.split('/')?.[0] || app.author || null;
 
37
  ? new Date(lastModified).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
38
  : null;
39
 
40
+ const handleInstallClick = (e) => {
41
+ e.preventDefault();
42
+ e.stopPropagation();
43
+ onInstallClick?.(app);
44
+ };
45
 
46
  return (
47
  <Box
 
 
 
 
48
  sx={{
49
  display: 'flex',
50
  flexDirection: 'column',
 
53
  overflow: 'hidden',
54
  bgcolor: '#ffffff',
55
  border: isOfficial ? '1px solid rgba(59, 130, 246, 0.25)' : '1px solid rgba(0, 0, 0, 0.08)',
 
56
  transition: 'all 0.2s ease',
 
57
  '&:hover': {
58
  transform: 'translateY(-4px)',
59
  boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
 
203
  {cardData.short_description || app.description || 'No description'}
204
  </Typography>
205
 
206
+ {/* Date + Install Button */}
207
  <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
208
+ {/* Date */}
209
+ {formattedDate ? (
210
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
211
  <AccessTimeIcon sx={{ fontSize: 14, color: '#999' }} />
212
  <Typography
 
219
  {formattedDate}
220
  </Typography>
221
  </Box>
222
+ ) : (
223
+ <Box />
224
  )}
225
 
226
+ {/* Install Button */}
227
+ <Button
228
+ variant="text"
229
+ size="small"
230
+ endIcon={<DownloadIcon sx={{ fontSize: 14 }} />}
231
+ onClick={handleInstallClick}
232
+ sx={{
233
+ py: 0.5,
234
+ px: 1.5,
235
+ borderRadius: '8px',
236
+ color: '#FF9500',
237
+ fontWeight: 600,
238
+ fontSize: 12,
239
+ textTransform: 'none',
240
+ '&:hover': {
241
+ bgcolor: 'rgba(255, 149, 0, 0.08)',
242
+ },
243
+ }}
244
+ >
245
+ Install
246
+ </Button>
247
  </Box>
248
  </Box>
249
  </Box>
 
256
  const { apps, loading, error } = useApps();
257
  const [searchQuery, setSearchQuery] = useState('');
258
  const [officialOnly, setOfficialOnly] = useState(false);
259
+
260
+ // Install modal state
261
+ const [installModalOpen, setInstallModalOpen] = useState(false);
262
+ const [selectedApp, setSelectedApp] = useState(null);
263
+
264
+ const handleInstallClick = (app) => {
265
+ setSelectedApp(app);
266
+ setInstallModalOpen(true);
267
+ };
268
+
269
+ const handleCloseInstallModal = () => {
270
+ setInstallModalOpen(false);
271
+ setSelectedApp(null);
272
+ };
273
 
274
  // Filter apps based on search and official toggle
275
  const filteredApps = useMemo(() => {
 
541
  <AppCard
542
  key={app.id || index}
543
  app={app}
544
+ onInstallClick={handleInstallClick}
545
  />
546
  ))}
547
  </Box>
548
  )}
549
  </Container>
550
+
551
+ {/* Install Modal */}
552
+ <InstallModal
553
+ open={installModalOpen}
554
+ onClose={handleCloseInstallModal}
555
+ app={selectedApp}
556
+ />
557
  </Layout>
558
  );
559
  }