/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ /** @jsxRuntime automatic */ /** @jsxImportSource hono/jsx */ import {AndroidIcon} from '@fluxer/marketing/src/components/icons/AndroidIcon'; import {AppleIcon} from '@fluxer/marketing/src/components/icons/AppleIcon'; import {CaretDownIcon} from '@fluxer/marketing/src/components/icons/CaretDownIcon'; import {DownloadIcon} from '@fluxer/marketing/src/components/icons/DownloadIcon'; import {LinuxIcon} from '@fluxer/marketing/src/components/icons/LinuxIcon'; import {WindowsIcon} from '@fluxer/marketing/src/components/icons/WindowsIcon'; import type {MarketingContext, MarketingPlatform} from '@fluxer/marketing/src/MarketingContext'; import {apiUrl, href, isCanary} from '@fluxer/marketing/src/UrlUtils'; export type ButtonStyle = 'light' | 'dark'; const lightBg = 'bg-white'; const lightText = 'text-[#4641D9]'; const lightHover = 'hover:bg-gray-50'; const darkBg = 'bg-[#4641D9]'; const darkText = 'text-white'; const darkHover = 'hover:bg-[#3a36b0]'; const btnSizing = 'px-5 py-3 md:px-6 md:py-3.5'; const btnBase = `download-link flex items-center justify-center rounded-l-2xl ${btnSizing} transition-colors shadow-lg`; const chevronBase = 'overlay-toggle flex items-center self-stretch rounded-r-2xl px-3 transition-colors shadow-lg'; const mobileBtnBase = `inline-flex items-center justify-center rounded-2xl ${btnSizing} transition-colors shadow-lg`; const secondaryBtnBase = `hidden items-center justify-center gap-2 rounded-2xl ${btnSizing} font-semibold text-sm text-white shadow-lg ring-1 ring-inset ring-white/30 bg-white/10 backdrop-blur-sm transition-colors hover:bg-white/20 sm:inline-flex md:text-base`; interface PlatformDownloadInfo { url: string; label: string; icon: JSX.Element; } export function getPlatformDownloadInfo(ctx: MarketingContext): PlatformDownloadInfo { switch (ctx.platform) { case 'windows': { const arch = defaultArchitecture(ctx, 'windows'); return { url: desktopRedirectUrl(ctx, 'win32', arch, 'setup'), label: ctx.i18n.getMessage('platform_support.platforms.windows.download_label', ctx.locale), icon: , }; } case 'macos': { const arch = defaultArchitecture(ctx, 'macos'); return { url: desktopRedirectUrl(ctx, 'darwin', arch, 'dmg'), label: ctx.i18n.getMessage('platform_support.platforms.macos.download_label', ctx.locale), icon: , }; } case 'linux': { const arch = defaultArchitecture(ctx, 'linux'); return { url: desktopRedirectUrl(ctx, 'linux', arch, 'deb'), label: ctx.i18n.getMessage('platform_support.platforms.linux.choose_distribution', ctx.locale), icon: , }; } case 'ios': case 'android': return { url: href(ctx, '/download'), label: ctx.i18n.getMessage('platform_support.mobile.mobile_apps_underway', ctx.locale), icon: , }; default: return { url: href(ctx, '/download'), label: ctx.i18n.getMessage('download.download', ctx.locale), icon: , }; } } export function getSystemRequirements(ctx: MarketingContext, platform: MarketingPlatform): string { switch (platform) { case 'windows': return ctx.i18n.getMessage('platform_support.platforms.windows.min_version', ctx.locale); case 'macos': return ctx.i18n.getMessage('platform_support.platforms.macos.min_version', ctx.locale); case 'linux': return ''; case 'ios': return ctx.i18n.getMessage('platform_support.platforms.ios.min_version', ctx.locale); case 'android': return ctx.i18n.getMessage('platform_support.platforms.android.min_version', ctx.locale); default: return ''; } } export function renderSecondaryButton(_ctx: MarketingContext, href: string, label: string): JSX.Element { return ( {label} ); } export function renderWithOverlay(ctx: MarketingContext, idPrefix: string | null = null): JSX.Element { const requirements = getSystemRequirements(ctx, ctx.platform); let button: JSX.Element; switch (ctx.platform) { case 'windows': button = renderDesktopButton(ctx, 'windows', 'light', idPrefix, false, false); break; case 'macos': button = renderDesktopButton(ctx, 'macos', 'light', idPrefix, false, false); break; case 'linux': button = renderDesktopButton(ctx, 'linux', 'light', idPrefix, false, false); break; case 'ios': case 'android': button = renderMobileRedirectButton(ctx, 'light'); break; default: button = ( {ctx.i18n.getMessage('download.download_fluxer', ctx.locale)} ); break; } if (!requirements) return button; return (
{button}

{requirements}

); } export function renderMobileButton( ctx: MarketingContext, platform: MarketingPlatform, style: ButtonStyle, ): JSX.Element { const config = getMobileConfig(ctx, platform); if (!config) return ; const [btnClass] = getMobileButtonClasses(style); const downloadFor = ctx.i18n.getMessage('download.download_for_prefix', ctx.locale); return ( {config.icon} {downloadFor} {config.platformName} ); } function renderMobileRedirectButton(ctx: MarketingContext, style: ButtonStyle): JSX.Element { const [btnClass] = getMobileButtonClasses(style); return ( {ctx.i18n.getMessage('platform_support.mobile.mobile_apps_underway', ctx.locale)} ); } function getMobileButtonClasses(style: ButtonStyle): [string] { if (style === 'light') { return [`${mobileBtnBase} gap-2 ${lightBg} ${lightText} hover:bg-white/90`]; } return [`${mobileBtnBase} gap-2 ${darkBg} ${darkText} ${darkHover}`]; } interface MobileButtonConfig { platformName: string; icon: JSX.Element; url: string; helperText: string; } function getMobileConfig(ctx: MarketingContext, platform: MarketingPlatform): MobileButtonConfig | null { switch (platform) { case 'ios': return { platformName: ctx.i18n.getMessage('platform_support.platforms.ios.name', ctx.locale), icon: , url: apiUrl(ctx, '/dl/ios/testflight'), helperText: ctx.i18n.getMessage('platform_support.platforms.ios.testflight', ctx.locale), }; case 'android': return { platformName: ctx.i18n.getMessage('platform_support.platforms.android.name', ctx.locale), icon: , url: apiUrl(ctx, '/dl/android/arm64/apk'), helperText: ctx.i18n.getMessage('platform_support.platforms.android.apk', ctx.locale), }; default: return null; } } export function renderDesktopButton( ctx: MarketingContext, platform: MarketingPlatform, style: ButtonStyle, idPrefix: string | null, compact: boolean, fullWidth: boolean, ): JSX.Element { const {platformId, platformName, icon, options} = getPlatformConfig(ctx, platform); const finalId = idPrefix ? `${idPrefix}-${platformId}` : platformId; const defaultArch = defaultArchitecture(ctx, platform); const selected = options.find((opt) => opt.arch === defaultArch) ?? options[0]; const [btnClass, chevronClass] = getDesktopButtonClasses(style); const containerClass = fullWidth ? 'flex w-full' : 'flex'; const widthModifier = fullWidth ? ' flex-1 w-full min-w-0' : ''; const buttonClass = `${btnClass}${widthModifier}`; const buttonLabel = compact ? platformName : `${ctx.i18n.getMessage('download.download_for_prefix', ctx.locale)}${platformName}`; return ( ); } interface PlatformOption { arch: string; format: string; url: string; } interface PlatformConfig { platformId: string; platformName: string; icon: JSX.Element; options: ReadonlyArray; } function getPlatformConfig(ctx: MarketingContext, platform: MarketingPlatform): PlatformConfig { switch (platform) { case 'windows': return { platformId: 'windows', platformName: ctx.i18n.getMessage('platform_support.platforms.windows.name', ctx.locale), icon: , options: [ {arch: 'x64', format: 'EXE', url: desktopRedirectUrl(ctx, 'win32', 'x64', 'setup')}, {arch: 'arm64', format: 'EXE', url: desktopRedirectUrl(ctx, 'win32', 'arm64', 'setup')}, ], }; case 'macos': return { platformId: 'macos', platformName: ctx.i18n.getMessage('platform_support.platforms.macos.name', ctx.locale), icon: , options: [ {arch: 'arm64', format: 'DMG', url: desktopRedirectUrl(ctx, 'darwin', 'arm64', 'dmg')}, {arch: 'x64', format: 'DMG', url: desktopRedirectUrl(ctx, 'darwin', 'x64', 'dmg')}, ], }; case 'linux': return { platformId: 'linux', platformName: ctx.i18n.getMessage('platform_support.platforms.linux.name', ctx.locale), icon: , options: linuxDownloadOptions(ctx), }; default: return {platformId: '', platformName: '', icon: , options: []}; } } function linuxDownloadOptions(ctx: MarketingContext): ReadonlyArray { return [ {arch: 'x64', format: 'AppImage', url: desktopRedirectUrl(ctx, 'linux', 'x64', 'appimage')}, {arch: 'arm64', format: 'AppImage', url: desktopRedirectUrl(ctx, 'linux', 'arm64', 'appimage')}, {arch: 'x64', format: 'DEB', url: desktopRedirectUrl(ctx, 'linux', 'x64', 'deb')}, {arch: 'arm64', format: 'DEB', url: desktopRedirectUrl(ctx, 'linux', 'arm64', 'deb')}, {arch: 'x64', format: 'RPM', url: desktopRedirectUrl(ctx, 'linux', 'x64', 'rpm')}, {arch: 'arm64', format: 'RPM', url: desktopRedirectUrl(ctx, 'linux', 'arm64', 'rpm')}, {arch: 'x64', format: 'tar.gz', url: desktopRedirectUrl(ctx, 'linux', 'x64', 'tar_gz')}, {arch: 'arm64', format: 'tar.gz', url: desktopRedirectUrl(ctx, 'linux', 'arm64', 'tar_gz')}, ]; } function getDesktopButtonClasses(style: ButtonStyle): [string, string] { if (style === 'light') { return [ `${btnBase} gap-2 ${lightBg} ${lightText} ${lightHover}`, `${chevronBase} ${lightBg} border-l border-gray-200 ${lightText} ${lightHover}`, ]; } return [ `${btnBase} gap-2 ${darkBg} ${darkText} ${darkHover}`, `${chevronBase} ${darkBg} border-l border-white/20 ${darkText} ${darkHover}`, ]; } function formatOverlayLabel(ctx: MarketingContext, platform: MarketingPlatform, arch: string, format: string): string { if (platform === 'macos') { return arch === 'arm64' ? `${ctx.i18n.getMessage('platform_support.platforms.macos.apple_silicon', ctx.locale)} (${format})` : `${ctx.i18n.getMessage('platform_support.platforms.macos.intel', ctx.locale)} (${format})`; } return `${format} (${arch})`; } function defaultArchitecture(ctx: MarketingContext, platform: MarketingPlatform): string { if (platform === 'macos') { if (ctx.architecture === 'arm64') return 'arm64'; if (ctx.architecture === 'unknown') return 'arm64'; return 'x64'; } if (ctx.architecture === 'arm64') return 'arm64'; return 'x64'; } function channelSegment(ctx: MarketingContext): string { return isCanary(ctx) ? 'canary' : 'stable'; } function desktopRedirectUrl(ctx: MarketingContext, platform: string, arch: string, format: string): string { return apiUrl(ctx, `/dl/desktop/${channelSegment(ctx)}/${platform}/${arch}/latest/${format}`); }