package component
import (
"fmt"
"strings"
"time"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type Release struct {
TagName string
Version string
CanaryBuild int
Date string
PublishedAt time.Time
IsCanary bool
Assets map[string]map[string]string
}
func (r Release) DownloadURL(os, arch string) string {
if m := r.Assets[os]; m != nil {
return m[arch]
}
return ""
}
func (r Release) DisplayVersion() string {
if r.CanaryBuild > 0 {
return fmt.Sprintf("%s Canary #%d", r.Version, r.CanaryBuild)
}
return r.Version
}
type DownloadProps struct {
DetectedOS string
DetectedArch string
SelectedOS string
SelectedArch string
ArchExplicit bool
Releases []Release
}
type LinuxFormat struct {
OS string
Label string
Archs []string
}
type DownloadConfig struct {
DefaultLinuxOS string
Formats []LinuxFormat
}
func (dc DownloadConfig) formatSupportsArch(os, arch string) bool {
for _, f := range dc.Formats {
if f.OS == os {
return containsString(f.Archs, arch)
}
}
return true
}
func containsString(list []string, s string) bool {
for _, v := range list {
if v == s {
return true
}
}
return false
}
type platformOption struct {
OS string
Arch string
Label string
}
var platforms = []platformOption{
{"mac", "arm64", "macOS ยท Apple Silicon"},
{"mac", "x64", "macOS ยท Intel"},
{"windows", "x64", "Windows ยท x64"},
{"linux", "x64", "Linux ยท x64"},
{"linux", "arm64", "Linux ยท ARM64"},
}
func fileExt(os string) string {
switch os {
case "windows":
return ".exe"
case "linux":
return ".AppImage"
case "linux-deb":
return ".deb"
case "linux-rpm":
return ".rpm"
case "linux-snap":
return ".snap"
case "linux-pacman":
return ".pacman"
case "linux-flatpak":
return ".flatpak"
default:
return ".dmg"
}
}
func platformLabel(os, arch string) string {
effectiveOS := os
if strings.HasPrefix(os, "linux") {
effectiveOS = "linux"
}
for _, p := range platforms {
if p.OS == effectiveOS && p.Arch == arch {
return p.Label
}
}
return effectiveOS + " ยท " + arch
}
func DownloadPage(b Branding, dc DownloadConfig, props PageProps, dl DownloadProps) Node {
props.Title = "Download โ " + b.Name
props.Description = "Download " + b.Name + " for macOS, Windows, or Linux."
var stable, canary []Release
for _, r := range dl.Releases {
if r.IsCanary {
canary = append(canary, r)
} else {
stable = append(stable, r)
}
}
content := []Node{
If(dl.SelectedOS == "mac" && !dl.ArchExplicit, macArchDetectScript()),
H1(Class("font-mono text-3xl font-bold text-white"), Text("Download "+b.Name)),
Div(Class("mt-8"),
P(Class("font-mono text-xs uppercase tracking-widest text-zinc-500 mb-3"), Text("Platform")),
Div(Class("flex flex-wrap gap-2"),
Group(platformButtons(dc, dl)),
),
If(strings.HasPrefix(dl.SelectedOS, "linux"),
linuxFormatToggle(dc, dl),
),
),
}
if len(stable) > 0 {
content = append(content, stableSection(b, stable, dl))
}
if len(canary) > 0 {
content = append(content, canarySection(b, canary, dl))
}
content = append(content,
Div(Class("mt-6 text-center"),
A(
Href(b.GitHubURL+"/releases"),
Class("font-mono text-xs text-zinc-500 hover:text-white transition-colors underline underline-offset-4"),
Text("All releases on GitHub โ"),
),
),
)
return Page(b, props,
Div(Class("mx-auto max-w-xl py-16"),
Group(content),
),
)
}
func stableSection(b Branding, stable []Release, dl DownloadProps) Node {
latest := stable[0]
older := stable[1:]
url := latest.DownloadURL(dl.SelectedOS, dl.SelectedArch)
return Group{
Div(Class("mt-10"),
Div(Class("mb-4 flex items-center gap-3"),
P(Class("font-mono text-xs uppercase tracking-widest text-seafoam-400"), Text("Stable releases")),
),
Div(Class("min-h-[200px] flex items-center rounded-xl border border-seafoam-400/30 bg-zinc-900 p-6"),
Div(Class("flex w-full items-center justify-between gap-4 flex-wrap"),
Div(
Span(Class("font-mono text-xs font-semibold uppercase tracking-widest text-seafoam-400"),
Text("Latest Stable"),
),
H2(Class("mt-1 font-mono text-xl font-bold text-white"),
Text(b.Name+" v"+latest.DisplayVersion()),
),
P(Class("mt-1 font-mono text-xs text-zinc-500"),
Text(latest.Date+" ยท "+platformLabel(dl.SelectedOS, dl.SelectedArch)+fileExt(dl.SelectedOS)),
),
),
Div(Class("flex-1")),
downloadButton(url, "bg-seafoam-400 hover:bg-seafoam-500 text-black"),
),
),
),
If(len(older) > 0,
Div(Class("mt-8"),
P(Class("font-mono text-xs uppercase tracking-widest text-zinc-500 mb-4"), Text("Older releases")),
Div(Class("divide-y divide-zinc-800 rounded-xl border border-zinc-800"),
Group(releaseRows(older, dl, "text-seafoam-400 hover:text-seafoam-500")),
),
),
),
}
}
func canarySection(b Branding, canary []Release, dl DownloadProps) Node {
latest := canary[0]
older := canary[1:]
url := latest.DownloadURL(dl.SelectedOS, dl.SelectedArch)
return Div(Class("mt-10"),
Div(Class("mb-4 flex items-center gap-3"),
P(Class("font-mono text-xs uppercase tracking-widest text-canary-500"), Text("Canary releases")),
Span(Class("font-mono text-xs text-zinc-600"), Text("โ unstable, may contain bugs")),
),
Div(Class("rounded-xl border border-canary-500/30 bg-zinc-900 p-6"),
Div(Class("flex items-center justify-between gap-4 flex-wrap"),
Div(
Span(Class("font-mono text-xs font-semibold uppercase tracking-widest text-canary-400"),
Text("Latest Canary"),
),
H2(Class("mt-1 font-mono text-base font-bold text-white"),
Text(b.Name+" v"+latest.DisplayVersion()),
),
P(Class("mt-1 font-mono text-xs text-zinc-500"),
Text(latest.Date+" ยท "+platformLabel(dl.SelectedOS, dl.SelectedArch)+fileExt(dl.SelectedOS)),
),
),
downloadButton(url, "bg-canary-500 hover:bg-canary-400 text-black", "py-2 text-xs"),
),
),
If(len(older) > 0,
Div(Class("mt-4 divide-y divide-zinc-800 rounded-xl border border-zinc-800"),
Group(releaseRows(older, dl, "text-canary-400 hover:text-canary-300")),
),
),
)
}
func downloadButton(url, colorCls string, extraCls ...string) Node {
base := "inline-flex items-center justify-center gap-2 rounded-lg w-[135px] font-mono font-semibold transition-colors shrink-0 "
sizeCls := "px-6 py-3 text-sm"
if len(extraCls) > 0 && extraCls[0] != "" {
sizeCls = extraCls[0]
}
btnCls := base + sizeCls + " " + colorCls
unavailableCls := base + sizeCls + " border border-zinc-800 text-zinc-600"
if url == "" {
return Span(Class(unavailableCls), Text("Not available"))
}
return A(
Href(url),
Class(btnCls),
Text("โ Download"),
)
}
func releaseRows(releases []Release, dl DownloadProps, linkCls string) []Node {
nodes := make([]Node, len(releases))
for i, r := range releases {
url := r.DownloadURL(dl.SelectedOS, dl.SelectedArch)
var link Node
if url != "" {
link = A(
Href(url),
Class("font-mono text-xs transition-colors "+linkCls),
Text("โ "+platformLabel(dl.SelectedOS, dl.SelectedArch)+fileExt(dl.SelectedOS)),
)
} else {
link = Span(
Class("font-mono text-xs text-zinc-600"),
Text("Not available"),
)
}
nodes[i] = Div(Class("flex items-center justify-between px-5 py-4"),
Div(Class("flex items-center gap-4"),
Span(Class("font-mono text-sm font-semibold text-white"), Text("v"+r.DisplayVersion())),
Span(Class("font-mono text-xs text-zinc-500"), Text(r.Date)),
),
link,
)
}
return nodes
}
func platformButtons(dc DownloadConfig, dl DownloadProps) []Node {
nodes := make([]Node, len(platforms))
effectiveSelectedOS := dl.SelectedOS
if strings.HasPrefix(dl.SelectedOS, "linux") {
effectiveSelectedOS = "linux"
}
effectiveDetectedOS := dl.DetectedOS
if strings.HasPrefix(dl.DetectedOS, "linux") {
effectiveDetectedOS = "linux"
}
for i, p := range platforms {
isSelected := p.OS == effectiveSelectedOS && p.Arch == dl.SelectedArch
isDetected := p.OS == effectiveDetectedOS && p.Arch == dl.DetectedArch
targetOS := p.OS
if p.OS == "linux" && strings.HasPrefix(dl.SelectedOS, "linux-") && dc.formatSupportsArch(dl.SelectedOS, p.Arch) {
targetOS = dl.SelectedOS
}
var cls string
if isSelected {
cls = "inline-flex items-center gap-1.5 rounded-md bg-primary-500 px-3 py-1.5 font-mono text-xs font-semibold text-black"
} else {
cls = "inline-flex items-center gap-1.5 rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 font-mono text-xs text-zinc-300 hover:border-zinc-500 hover:text-white transition-colors"
}
children := []Node{Text(p.Label)}
if isDetected && !isSelected {
children = append(children,
Span(Class("rounded bg-zinc-700 px-1 py-0.5 text-zinc-400 text-[10px]"), Text("auto")),
)
} else if isDetected && isSelected {
children = append(children,
Span(Class("rounded bg-black/20 px-1 py-0.5 text-[10px]"), Text("auto")),
)
}
nodes[i] = A(
Href(fmt.Sprintf("/download?os=%s&arch=%s", targetOS, p.Arch)),
Class(cls),
Group(children),
)
}
return nodes
}
func macArchDetectScript() Node {
return Script(Raw(`(function(){
try {
var c = document.createElement('canvas');
var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
if (!gl) return;
var ext = gl.getExtension('WEBGL_debug_renderer_info');
if (!ext) return;
var renderer = String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) || '');
if (!renderer || /Apple/i.test(renderer)) return;
var url = new URL(window.location.href);
url.searchParams.set('arch', 'x64');
window.location.replace(url.toString());
} catch (e) {}
})();`))
}
func linuxFormatToggle(dc DownloadConfig, dl DownloadProps) Node {
selectedCls := "inline-flex items-center rounded-md bg-primary-500 px-3 py-1.5 font-mono text-xs font-semibold text-black"
unselectedCls := "inline-flex items-center rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 font-mono text-xs text-zinc-300 hover:border-zinc-500 hover:text-white transition-colors"
cls := func(targetOS string) string {
if dl.SelectedOS == targetOS {
return selectedCls
}
return unselectedCls
}
href := func(targetOS string) string {
return fmt.Sprintf("/download?os=%s&arch=%s", targetOS, dl.SelectedArch)
}
var buttons []Node
for _, f := range dc.Formats {
if !containsString(f.Archs, dl.SelectedArch) {
continue
}
buttons = append(buttons, A(Href(href(f.OS)), Class(cls(f.OS)), Text(f.Label)))
}
return Div(Class("mt-4"),
P(Class("font-mono text-xs uppercase tracking-widest text-zinc-500 mb-3"), Text("Format")),
Div(Class("flex items-center gap-2"),
Group(buttons),
),
)
}