diff --git a/apps/remix-ide/src/app/tabs/locales/en/desktopDownload.json b/apps/remix-ide/src/app/tabs/locales/en/desktopDownload.json new file mode 100644 index 00000000000..cfc956d218d --- /dev/null +++ b/apps/remix-ide/src/app/tabs/locales/en/desktopDownload.json @@ -0,0 +1,14 @@ +{ + "desktopDownload.loading": "Loading desktop app info...", + "desktopDownload.error": "Unable to load desktop app. Check the {link} for downloads.", + "desktopDownload.title": "Remix Desktop", + "desktopDownload.releaseDate": "Released {date}", + "desktopDownload.downloadSpan": "Download Remix Desktop {platform} {version}", + "desktopDownload.downloadSpanGeneric": "Download Remix Desktop {version}", + "desktopDownload.downloadCompactFull": "Download Remix Desktop {platform} {version}", + "desktopDownload.downloadCompactGeneric": "Download Remix Desktop {version}", + "desktopDownload.downloadButton": "Download for {platform}", + "desktopDownload.viewReleases": "View Downloads", + "desktopDownload.otherVersions": "Other versions and platforms", + "desktopDownload.noAutoDetect": "Available for Windows, macOS, and Linux" +} diff --git a/libs/remix-ui/desktop-download/README.md b/libs/remix-ui/desktop-download/README.md new file mode 100644 index 00000000000..08a8f1d34bd --- /dev/null +++ b/libs/remix-ui/desktop-download/README.md @@ -0,0 +1,139 @@ +# Desktop Download Component + +A React component for downloading the latest Remix Desktop application from GitHub releases. + +## Features + +- **Auto OS Detection**: Automatically detects the user's operating system (Windows, macOS, Linux) and suggests the appropriate download +- **Architecture Support**: Detects Apple Silicon Macs and provides ARM64 builds when available +- **Smart Caching**: Caches release data for 30 minutes to reduce API calls +- **Internationalization**: Fully supports i18n with FormattedMessage components +- **Responsive Design**: Works on both desktop and mobile devices +- **Error Handling**: Graceful fallback when GitHub API is unavailable + +## Usage + +```tsx +import { DesktopDownload } from '@remix-ui/desktop-download' + +function MyComponent() { + return ( +
+ {/* Compact layout with tracking */} + + + {/* Full layout */} + + + {/* Span variant for dropdowns */} + +
+ ) +} +``` + +### Compact Layout (Default) +Perfect for navigation bars, toolbars, or anywhere space is limited. Shows a button with "Download Remix Desktop {OS} {version}" and a small muted link to other releases below. + +```tsx + + +``` + +### Full Layout +Great for landing pages, cards, or dedicated download sections. Shows detailed information including release date, file size, and platform-specific icons. + +```tsx + +``` + +### Span Variant +Perfect for dropdown items or anywhere you need a simple link without button styling. + +```tsx + +``` + +### With custom styling + +```tsx + + + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `className` | `string` | `''` | Additional CSS classes | +| `compact` | `boolean` | `true` | Use compact layout | +| `variant` | `'button' \| 'span'` | `'button'` | Display variant | +| `style` | `CSSProperties` | `{}` | Inline styles | +| `trackingContext` | `string` | `'unknown'` | Context for Matomo analytics (e.g., 'hometab', 'dropdown', 'navbar') | + +## Analytics Tracking + +The component includes automatic Matomo analytics tracking for all download interactions. Set the `trackingContext` prop to identify where the component is used: + +```tsx + +``` + +**Tracking Events:** +- Category: `desktopDownload` +- Action: `{context}-{variant}` (e.g., `hometab-compact`, `dropdown-span`) +- Name: `{platform}-{filename}` or `releases-page` for fallbacks + +**Examples:** +- `['trackEvent', 'desktopDownload', 'hometab-compact', 'linux-remix-desktop_1.1.0_amd64.deb']` +- `['trackEvent', 'desktopDownload', 'dropdown-span', 'windows-remix-desktop-1.1.0-setup.exe']` +- `['trackEvent', 'desktopDownload', 'navbar-full-fallback', 'releases-page']` + +## Platform Support + +The component automatically detects and provides downloads for: + +- **Windows**: `.exe` installer +- **macOS**: `.dmg` disk image (ARM64 for Apple Silicon, Intel for older Macs) +- **Linux**: `.deb` package (with `.AppImage` fallback) + +## API + +The component fetches release data from: +`https://api.github.com/repos/remix-project-org/remix-desktop/releases/latest` + +## Caching + +Release data is cached in localStorage for 30 minutes to reduce GitHub API calls and improve performance. + +## Dependencies + +- React 18+ +- @remix-ui/helper (for CustomTooltip) +- react-intl (for internationalization) +- Bootstrap CSS classes (for styling) +- FontAwesome icons (for platform icons) + +## Internationalization + +The component is fully internationalized using react-intl. Translation strings are located in: + +- `apps/remix-ide/src/app/tabs/locales/en/desktopDownload.json` - All translation keys for the component + +### Translation Keys + +| Key | Default Message | Description | +|-----|-----------------|-------------| +| `desktopDownload.loading` | "Loading desktop app info..." | Loading state message | +| `desktopDownload.error` | "Unable to load desktop app. Check the {link} for downloads." | Error fallback message | +| `desktopDownload.title` | "Remix Desktop" | Main title in full layout | +| `desktopDownload.releaseDate` | "Released {date}" | Release date display | +| `desktopDownload.downloadSpan` | "Download Remix Desktop {platform} {version}" | Span variant with platform | +| `desktopDownload.downloadSpanGeneric` | "Download Remix Desktop {version}" | Span variant without platform | +| `desktopDownload.downloadCompactFull` | "Download Remix Desktop {platform} {version}" | Compact button with platform | +| `desktopDownload.downloadCompactGeneric` | "Download Remix Desktop {version}" | Compact button without platform | +| `desktopDownload.downloadButton` | "Download for {platform}" | Full layout button text | +| `desktopDownload.viewReleases` | "View Downloads" | Fallback button text | +| `desktopDownload.otherVersions` | "Other versions and platforms" | Link to releases page | +| `desktopDownload.noAutoDetect` | "Available for Windows, macOS, and Linux" | Platform availability message | diff --git a/libs/remix-ui/desktop-download/index.ts b/libs/remix-ui/desktop-download/index.ts new file mode 100644 index 00000000000..5f59ba73223 --- /dev/null +++ b/libs/remix-ui/desktop-download/index.ts @@ -0,0 +1 @@ +export * from './lib/desktop-download' \ No newline at end of file diff --git a/libs/remix-ui/desktop-download/lib/desktop-download.css b/libs/remix-ui/desktop-download/lib/desktop-download.css new file mode 100644 index 00000000000..2e402d4b5a3 --- /dev/null +++ b/libs/remix-ui/desktop-download/lib/desktop-download.css @@ -0,0 +1,98 @@ +.desktop-download { + max-width: 350px; +} + +.desktop-download.compact { + max-width: none; +} + +.desktop-download.compact .btn { + white-space: normal; /* Allow text wrapping */ + text-align: center; + line-height: 1.3; + min-height: 2.5rem; /* Ensure consistent button height */ + display: flex; + align-items: center; + justify-content: center; +} + +.desktop-download.compact .text-muted { + font-size: 0.75rem; + text-align: center; +} + +.desktop-download.span-variant { + max-width: none; +} + +.desktop-download.span-variant a { + color: inherit !important; + text-decoration: none !important; + position: relative; + z-index: 10; +} + +.desktop-download.span-variant a:hover { + color: inherit !important; + opacity: 0.8; +} + +.desktop-download.span-variant .d-flex { + pointer-events: auto; +} + +.desktop-download .btn { + border-radius: 8px; + font-weight: 500; + transition: all 0.2s ease; + white-space: normal; /* Allow text wrapping on all buttons */ + text-align: center; + line-height: 1.3; +} + +.desktop-download .btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.desktop-download .badge { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; +} + +.desktop-download .text-muted a:hover { + text-decoration: underline !important; +} + +.desktop-download .spinner-border-sm { + width: 1rem; + height: 1rem; +} + +.desktop-download .fab, +.desktop-download .fas, +.desktop-download .far { + min-width: 1.2rem; +} + +/* Dark mode support - removed custom colors to use app defaults */ + +@media (max-width: 768px) { + .desktop-download.full { + max-width: 100%; + } + + .desktop-download.full .btn { + width: 100%; + justify-content: center; + } + + .desktop-download.full h5 { + font-size: 1.1rem; + } + + .desktop-download.compact .btn { + font-size: 0.875rem; + } +} diff --git a/libs/remix-ui/desktop-download/lib/desktop-download.tsx b/libs/remix-ui/desktop-download/lib/desktop-download.tsx new file mode 100644 index 00000000000..e03ce39dfd9 --- /dev/null +++ b/libs/remix-ui/desktop-download/lib/desktop-download.tsx @@ -0,0 +1,522 @@ +import React, { useState, useEffect } from 'react' +import { CustomTooltip } from '@remix-ui/helper' +import { FormattedMessage } from 'react-intl' +import './desktop-download.css' + +const _paq = (window._paq = window._paq || []) // eslint-disable-line + +interface DesktopDownloadProps { + className?: string + compact?: boolean + variant?: 'button' | 'span' | 'auto' + style?: React.CSSProperties + trackingContext?: string // Context for Matomo tracking (e.g., 'hometab', 'dropdown', 'navbar') +} + +interface ReleaseAsset { + name: string + browser_download_url: string + size: number +} + +interface ReleaseData { + tag_name: string + name: string + published_at: string + html_url: string + assets: ReleaseAsset[] +} + +interface DetectedDownload { + url: string + filename: string + platform: string + size: number +} + +const GITHUB_API_URL = 'https://api.github.com/repos/remix-project-org/remix-desktop/releases/latest' +const CACHE_KEY = 'remix-desktop-release-cache' +const CACHE_DURATION = 30 * 60 * 1000 // 30 minutes in milliseconds + +export const DesktopDownload: React.FC = ({ + className = '', + compact = true, + variant = 'button', + style = {}, + trackingContext = 'unknown' +}) => { + const [releaseData, setReleaseData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [detectedDownload, setDetectedDownload] = useState(null) + + // Detect user's operating system + const detectOS = (): 'windows' | 'macos' | 'linux' => { + const userAgent = navigator.userAgent.toLowerCase() + if (userAgent.includes('win')) return 'windows' + if (userAgent.includes('mac')) return 'macos' + return 'linux' + } + + // Check if cached data is still valid + const getCachedData = (): ReleaseData | null => { + try { + const cached = localStorage.getItem(CACHE_KEY) + if (!cached) return null + + const { data, timestamp } = JSON.parse(cached) + if (Date.now() - timestamp > CACHE_DURATION) { + localStorage.removeItem(CACHE_KEY) + return null + } + + return data + } catch { + localStorage.removeItem(CACHE_KEY) + return null + } + } + + // Cache the release data + const setCachedData = (data: ReleaseData) => { + try { + localStorage.setItem(CACHE_KEY, JSON.stringify({ + data, + timestamp: Date.now() + })) + } catch { + // Ignore storage errors + } + } + + // Fetch release data from GitHub API + const fetchReleaseData = async (): Promise => { + const response = await fetch(GITHUB_API_URL) + if (!response.ok) { + throw new Error(`Failed to fetch release data: ${response.status}`) + } + return response.json() + } + + // Find the appropriate download for the user's OS + const findDownloadForOS = (assets: ReleaseAsset[], os: string): DetectedDownload | null => { + let preferredAsset: ReleaseAsset | null = null + + if (os === 'windows') { + preferredAsset = assets.find(asset => + asset.name.toLowerCase().includes('setup') && + asset.name.toLowerCase().includes('.exe') + ) || null + } else if (os === 'macos') { + // Check if user is on Apple Silicon (M1/M2/etc.) + const isAppleSilicon = navigator.userAgent.includes('Macintosh') && + (navigator.userAgent.includes('ARM') || + /Mac OS X 10_1[5-9]|Mac OS X 1[1-9]|macOS/.test(navigator.userAgent)) + + // Prefer ARM64 version for newer Macs, Intel version for older ones + if (isAppleSilicon) { + preferredAsset = assets.find(asset => + asset.name.toLowerCase().includes('.dmg') && + asset.name.toLowerCase().includes('arm64') + ) || null + } + + // Fallback to any dmg if ARM64 not found or not Apple Silicon + if (!preferredAsset) { + preferredAsset = assets.find(asset => + asset.name.toLowerCase().includes('.dmg') && + !asset.name.toLowerCase().includes('arm64') + ) || assets.find(asset => + asset.name.toLowerCase().includes('.dmg') + ) || null + } + } else if (os === 'linux') { + // Prefer .deb for most Linux distributions + preferredAsset = assets.find(asset => + asset.name.toLowerCase().includes('.deb') + ) || null + + // Fallback to AppImage if no .deb found + if (!preferredAsset) { + preferredAsset = assets.find(asset => + asset.name.toLowerCase().includes('.appimage') + ) || null + } + } + + if (!preferredAsset) return null + + return { + url: preferredAsset.browser_download_url, + filename: preferredAsset.name, + platform: os, + size: preferredAsset.size + } + } + + // Format file size + const formatSize = (bytes: number): string => { + const mb = bytes / (1024 * 1024) + return `${mb.toFixed(1)} MB` + } + + // Format date + const formatDate = (dateString: string): string => { + const date = new Date(dateString) + return date.toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric' + }) + } + + // Get platform-specific icon + const getPlatformIcon = (platform: string): string => { + switch (platform) { + case 'windows': return 'fab fa-windows' + case 'macos': return 'fab fa-apple' + case 'linux': return 'fab fa-linux' + default: return 'fas fa-desktop' + } + } + + // Get platform display name + const getPlatformName = (platform: string): string => { + switch (platform) { + case 'windows': return 'Windows' + case 'macos': return 'macOS' + case 'linux': return 'Linux' + default: return platform.charAt(0).toUpperCase() + platform.slice(1) + } + } + + // Track download click events + const trackDownloadClick = (platform?: string, filename?: string, variant?: string) => { + const trackingData = [ + 'trackEvent', + 'desktopDownload', + `${trackingContext}-${variant || 'button'}`, + platform ? `${platform}-${filename}` : 'releases-page' + ] + _paq.push(trackingData) + } + + // Load release data on component mount + useEffect(() => { + const loadReleaseData = async () => { + try { + setLoading(true) + setError(null) + + // Try to get cached data first + const cached = getCachedData() + if (cached) { + setReleaseData(cached) + const os = detectOS() + const download = findDownloadForOS(cached.assets, os) + setDetectedDownload(download) + setLoading(false) + return + } + + // Fetch fresh data + const data = await fetchReleaseData() + setReleaseData(data) + setCachedData(data) + + // Find appropriate download for user's OS + const os = detectOS() + const download = findDownloadForOS(data.assets, os) + setDetectedDownload(download) + + } catch (err) { + console.error('Failed to load release data:', err) + setError(err instanceof Error ? err.message : 'Failed to load release data') + } finally { + setLoading(false) + } + } + + loadReleaseData() + }, []) + + if (loading) { + return ( +
+
+ Loading... +
+ + + +
+ ) + } + + if (error || !releaseData) { + return ( +
+ + + releases page + + ) + }} + /> + +
+ ) + } + + return ( +
+ {variant === 'span' ? ( + // Span variant - for use in dropdown items + + ) : compact ? ( + // Compact layout - single line with additional info below + + ) : ( + // Full layout - original multi-line design +
+
+
+ +
+ {releaseData.tag_name} +
+ +
+ + + +
+ + {detectedDownload ? ( + + ) : ( + + )} +
+ )} +
+ ) +} + +export default DesktopDownload diff --git a/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx b/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx index 1f9222ad8cf..843c9ebd38b 100644 --- a/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx +++ b/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx @@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl' import { CustomTooltip } from '@remix-ui/helper' import { ThemeContext } from '../themeContext' import { Placement } from 'react-bootstrap/esm/types' +import { DesktopDownload } from 'libs/remix-ui/desktop-download' // eslint-disable-line @nrwl/nx/enforce-module-boundaries const _paq = (window._paq = window._paq || []) // eslint-disable-line type HometabIconSection = { @@ -115,6 +116,7 @@ function HomeTabTitle() { _paq.push(['trackEvent', 'hometab', 'titleCard', 'documentation'])}> _paq.push(['trackEvent', 'hometab', 'titleCard', 'webSite'])}> + ) diff --git a/libs/remix-ui/top-bar/src/components/WorkspaceDropdown.tsx b/libs/remix-ui/top-bar/src/components/WorkspaceDropdown.tsx index 21d705a1a7b..a71c318e7b2 100644 --- a/libs/remix-ui/top-bar/src/components/WorkspaceDropdown.tsx +++ b/libs/remix-ui/top-bar/src/components/WorkspaceDropdown.tsx @@ -7,6 +7,7 @@ import { FiMoreVertical } from 'react-icons/fi' import { TopbarContext } from '../context/topbarContext' import { getWorkspaces } from 'libs/remix-ui/workspace/src/lib/actions' import { WorkspaceMetadata } from 'libs/remix-ui/workspace/src/lib/types' +import { DesktopDownload } from 'libs/remix-ui/desktop-download' interface Branch { name: string @@ -326,19 +327,8 @@ export const WorkspacesDropdown: React.FC = ({ menuItem - { - window.open('https://github.com/remix-project-org/remix-desktop/releases', '_blank') - setShowMain(false) - setOpenSub(null) - }}> - { - window.open('https://github.com/remix-project-org/remix-desktop/releases', '_blank') - setShowMain(false) - setOpenSub(null) - }}> - - Download Remix Desktop - + + { downloadWorkspaces()