App downloads
This commit is contained in:
1 parent
b02f710c76
commit
036e378ac7
6 files changed
+465
-59
No files matched your search
@@ -1,11 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ArrowDownToLineIcon } from "lucide-react";
|
||||
import {
|
||||
AppFamily,
|
||||
AppGallery,
|
||||
AppGetIt,
|
||||
} from "@/components/apps/app-sections";
|
||||
import { AppHeroDownloadButton } from "@/components/apps/app-downloads";
|
||||
import {
|
||||
AppIcon,
|
||||
AppTab,
|
||||
@@ -27,7 +27,9 @@ import {
|
||||
craftAppOgImage,
|
||||
craftAppPath,
|
||||
craftAppIconUrl,
|
||||
craftAppRelease,
|
||||
craftAppRepo,
|
||||
craftReleaseDownloads,
|
||||
craftReleasePageUrl,
|
||||
craftShotUrl,
|
||||
getCraftApp,
|
||||
@@ -73,6 +75,7 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
if (!app) notFound();
|
||||
|
||||
const name = craftAppName(app);
|
||||
const release = craftAppRelease(app);
|
||||
const [hero] = app.shots;
|
||||
const [before, accent, after] = app.headline;
|
||||
const shareUrl = siteUrl(craftAppPath(app));
|
||||
@@ -88,9 +91,9 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
thumbnailUrl: siteUrl(craftAppIconUrl(app)),
|
||||
screenshot: app.shots.map((shot) => siteUrl(craftShotUrl(app, shot))),
|
||||
sameAs: craftAppRepo(app),
|
||||
...(app.release && {
|
||||
softwareVersion: app.release.version,
|
||||
downloadUrl: craftReleasePageUrl(app, app.release),
|
||||
...(release && {
|
||||
softwareVersion: release.version,
|
||||
downloadUrl: craftReleasePageUrl(app, release),
|
||||
}),
|
||||
isAccessibleForFree: true,
|
||||
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
|
||||
@@ -140,15 +143,17 @@ export default async function CraftAppPage({ params }: { params: Params }) {
|
||||
>
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{app.release && (
|
||||
<Button href="#get-it" size="lg">
|
||||
<ArrowDownToLineIcon aria-hidden className="h-4 w-4" />
|
||||
Download {name}
|
||||
</Button>
|
||||
{release && (
|
||||
<AppHeroDownloadButton
|
||||
name={name}
|
||||
recommended={craftReleaseDownloads(app, release).filter(
|
||||
(download) => download.recommended,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<DiscordButton
|
||||
size="lg"
|
||||
variant={app.release ? "secondary" : "primary"}
|
||||
variant={release ? "secondary" : "primary"}
|
||||
>
|
||||
Join the Discord
|
||||
</DiscordButton>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Badge } from "@/components/ui";
|
||||
import {
|
||||
craftAppName,
|
||||
craftAppPath,
|
||||
craftAppRelease,
|
||||
craftShotUrl,
|
||||
type CraftApp,
|
||||
} from "@/lib/crafting-apps";
|
||||
@@ -24,6 +25,7 @@ export default function AppCard({
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const [hero] = app.shots;
|
||||
const release = craftAppRelease(app);
|
||||
return (
|
||||
<Link
|
||||
href={craftAppPath(app)}
|
||||
@@ -66,7 +68,9 @@ export default function AppCard({
|
||||
{!compact && (
|
||||
<div className="mt-6 flex flex-wrap gap-1.5">
|
||||
<Badge label={app.status} className="text-(--app-ink)" />
|
||||
{app.release && <Badge label="Installers ready" className="text-(--app-ink)" />}
|
||||
{release && (
|
||||
<Badge label="Installers ready" className="text-(--app-ink)" />
|
||||
)}
|
||||
{app.platforms.map((platform) => (
|
||||
<Badge key={platform} label={platform} />
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { ArrowDownToLineIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui";
|
||||
import {
|
||||
CRAFT_DESKTOP_PLATFORMS,
|
||||
type CraftDesktopPlatform,
|
||||
} from "@/lib/crafting-app-releases";
|
||||
import type { CraftDownload } from "@/lib/crafting-apps";
|
||||
|
||||
// OS-aware download buttons for /apps/<slug>. Detection runs after mount
|
||||
// (no user agent on the server), so the first paint offers every platform
|
||||
// equally and the "for your system" button arrives with hydration. Phones,
|
||||
// tablets and Chromebooks keep the neutral layout.
|
||||
|
||||
type DetectedDesktop = {
|
||||
platform: CraftDesktopPlatform;
|
||||
arch?: CraftDownload["arch"];
|
||||
};
|
||||
|
||||
/** Main buttons in the Get-it section: your platform first, then the rest. */
|
||||
export function AppDownloadButtons({
|
||||
recommended,
|
||||
version,
|
||||
}: {
|
||||
/** The release's `recommended` files. */
|
||||
recommended: CraftDownload[];
|
||||
version: string;
|
||||
}) {
|
||||
const detected = useDetectedDesktop();
|
||||
const mine = detected && pickDownload(recommended, detected);
|
||||
const defaults = CRAFT_DESKTOP_PLATFORMS.flatMap((platform) => {
|
||||
const download = pickDownload(recommended, { platform });
|
||||
return download ? [download] : [];
|
||||
});
|
||||
|
||||
if (!mine) {
|
||||
return (
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
{defaults.map((download) => (
|
||||
<Button key={download.href} href={download.href} size="lg">
|
||||
<ArrowDownToLineIcon aria-hidden className="h-4 w-4" />
|
||||
{download.group}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const others = defaults.filter((download) => download.group !== mine.group);
|
||||
return (
|
||||
<div className="mt-8 flex flex-col gap-3">
|
||||
<Button href={mine.href} size="lg">
|
||||
<ArrowDownToLineIcon aria-hidden className="h-4 w-4" />
|
||||
Download for {mine.group}
|
||||
</Button>
|
||||
<p className="hud-label text-faint">
|
||||
{mine.label} · v{version}
|
||||
</p>
|
||||
{others.length > 0 && (
|
||||
<div className="mt-3 flex flex-wrap gap-3">
|
||||
{others.map((download) => (
|
||||
<Button
|
||||
key={download.href}
|
||||
href={download.href}
|
||||
variant="secondary"
|
||||
size="md"
|
||||
>
|
||||
<ArrowDownToLineIcon aria-hidden className="h-3.5 w-3.5" />
|
||||
{download.group}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Hero CTA: a direct download once the OS is known, else a jump to #get-it. */
|
||||
export function AppHeroDownloadButton({
|
||||
recommended,
|
||||
name,
|
||||
}: {
|
||||
recommended: CraftDownload[];
|
||||
name: string;
|
||||
}) {
|
||||
const detected = useDetectedDesktop();
|
||||
const mine = detected && pickDownload(recommended, detected);
|
||||
return (
|
||||
<Button href={mine ? mine.href : "#get-it"} size="lg">
|
||||
<ArrowDownToLineIcon aria-hidden className="h-4 w-4" />
|
||||
{mine ? `Download for ${mine.group}` : `Download ${name}`}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function useDetectedDesktop(): DetectedDesktop | null {
|
||||
const [detected, setDetected] = useState<DetectedDesktop | null>(null);
|
||||
useEffect(() => {
|
||||
setDetected(detectDesktop());
|
||||
}, []);
|
||||
return detected;
|
||||
}
|
||||
|
||||
/** The platform's first recommended file, or one matching the CPU if any. */
|
||||
function pickDownload(
|
||||
recommended: CraftDownload[],
|
||||
{ platform, arch }: DetectedDesktop,
|
||||
): CraftDownload | undefined {
|
||||
const candidates = recommended.filter((download) => download.group === platform);
|
||||
return candidates.find((download) => arch && download.arch === arch) ?? candidates[0];
|
||||
}
|
||||
|
||||
function detectDesktop(): DetectedDesktop | null {
|
||||
const ua = navigator.userAgent;
|
||||
if (/Android|iPhone|iPad|iPod|Mobile|CrOS/i.test(ua)) return null;
|
||||
if (/Windows/i.test(ua)) return { platform: "Windows" };
|
||||
if (/Macintosh|Mac OS X/i.test(ua)) {
|
||||
// iPadOS Safari reports itself as a Mac; touch support gives it away.
|
||||
return navigator.maxTouchPoints > 1 ? null : { platform: "macOS" };
|
||||
}
|
||||
if (/Linux/i.test(ua)) {
|
||||
return {
|
||||
platform: "Linux",
|
||||
arch: /aarch64|arm64|armv8/i.test(ua) ? "aarch64" : "x86_64",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,28 +1,41 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowDownToLineIcon, ArrowUpRightIcon, TerminalIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
ArrowDownToLineIcon,
|
||||
ArrowUpRightIcon,
|
||||
ChevronDownIcon,
|
||||
TerminalIcon,
|
||||
} from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import DiscordButton from "@/components/discord-button";
|
||||
import { GitHubIcon } from "@/components/icons";
|
||||
import { SectionShell, SectionEyebrow } from "@/components/landing/section-shell";
|
||||
import { Badge, Button, CopyButton } from "@/components/ui";
|
||||
import { CRAFT_DESKTOP_PLATFORMS } from "@/lib/crafting-app-releases";
|
||||
import {
|
||||
CRAFTING_APPS,
|
||||
craftAppBuildCommand,
|
||||
craftAppIndex,
|
||||
craftAppName,
|
||||
craftAppRelease,
|
||||
craftAppRepo,
|
||||
craftReleaseDownloads,
|
||||
craftReleasePageUrl,
|
||||
craftReleasesUrl,
|
||||
craftShotSourceUrl,
|
||||
craftShotUrl,
|
||||
formatList,
|
||||
type CraftApp,
|
||||
type CraftDownload,
|
||||
} from "@/lib/crafting-apps";
|
||||
import AppCard from "./app-card";
|
||||
import { AppDownloadButtons } from "./app-downloads";
|
||||
import { ColorAccent } from "./app-wordmark";
|
||||
|
||||
// Body sections of an /apps/<slug> page, in page order. Server components;
|
||||
// the only client island is the CopyButton.
|
||||
// the client islands are the CopyButton and the OS-aware download buttons.
|
||||
|
||||
const DOWNLOAD_GROUPS = [...CRAFT_DESKTOP_PLATFORMS, "Other"] as const;
|
||||
|
||||
const CELL_HEADING_CLASSES =
|
||||
"mt-4 font-display text-3xl font-medium leading-[1.05] tracking-[-0.03em] text-ink-strong sm:text-4xl";
|
||||
@@ -93,12 +106,14 @@ export function AppGallery({ app, index }: { app: CraftApp; index: string }) {
|
||||
}
|
||||
|
||||
// Installers (or, until there are any, the Discord waitlist) beside
|
||||
// build-from-source. Giving an app a `release` in crafting-apps.ts flips the
|
||||
// left cell to download buttons.
|
||||
// build-from-source. Versions and files come from crafting-app-releases.ts;
|
||||
// a null release there flips the left cell to the waitlist.
|
||||
export function AppGetIt({ app, index }: { app: CraftApp; index: string }) {
|
||||
const name = craftAppName(app);
|
||||
const command = craftAppBuildCommand(app);
|
||||
const desktop = app.platforms.filter((platform) => platform !== "Web");
|
||||
const release = craftAppRelease(app);
|
||||
const downloads = release ? craftReleaseDownloads(app, release) : [];
|
||||
|
||||
return (
|
||||
<SectionShell id="get-it">
|
||||
@@ -110,32 +125,28 @@ export function AppGetIt({ app, index }: { app: CraftApp; index: string }) {
|
||||
<div className="grid gap-px bg-line md:grid-cols-2">
|
||||
<div data-reveal className="flex flex-col bg-bg p-6 md:p-10">
|
||||
<p className="hud-label text-faint">Installers</p>
|
||||
{app.release ? (
|
||||
{release ? (
|
||||
<>
|
||||
<h3 className={CELL_HEADING_CLASSES}>
|
||||
Download <ColorAccent>{name}</ColorAccent>.
|
||||
</h3>
|
||||
<p className="mt-4 max-w-md leading-relaxed text-muted">
|
||||
Version {app.release.version}, free. Questions or feedback?
|
||||
Version {release.version}, free. Questions or feedback?
|
||||
The team is in the ArtCraft Discord.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
{craftReleaseDownloads(app, app.release).map((download) => (
|
||||
<Button key={download.href} href={download.href} size="lg">
|
||||
<ArrowDownToLineIcon aria-hidden className="h-4 w-4" />
|
||||
{download.platform}
|
||||
</Button>
|
||||
))}
|
||||
<AppDownloadButtons
|
||||
recommended={downloads.filter((download) => download.recommended)}
|
||||
version={release.version}
|
||||
/>
|
||||
<AllDownloads downloads={downloads} />
|
||||
<div className="mt-5 flex flex-wrap gap-x-6 gap-y-2">
|
||||
<ExternalLink href={craftReleasePageUrl(app, release)}>
|
||||
Release notes on GitHub
|
||||
</ExternalLink>
|
||||
<ExternalLink href={craftReleasesUrl(app)}>
|
||||
All releases
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<a
|
||||
href={craftReleasePageUrl(app, app.release)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hud-label mt-5 flex w-fit items-center gap-1.5 text-muted hover:text-ink"
|
||||
>
|
||||
All builds and checksums
|
||||
<ArrowUpRightIcon aria-hidden className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -209,6 +220,67 @@ export function AppGetIt({ app, index }: { app: CraftApp; index: string }) {
|
||||
}
|
||||
|
||||
// The other apps, compact, linking back to the hub.
|
||||
|
||||
// Every file in the release, grouped by platform, behind a disclosure.
|
||||
function AllDownloads({ downloads }: { downloads: CraftDownload[] }) {
|
||||
return (
|
||||
<details className="group/all mt-6 border border-line">
|
||||
<summary className="hud-label flex cursor-pointer list-none items-center justify-between gap-2 px-4 py-3 text-muted hover:text-ink [&::-webkit-details-marker]:hidden">
|
||||
All downloads ({downloads.length} files)
|
||||
<ChevronDownIcon
|
||||
aria-hidden
|
||||
className="h-3.5 w-3.5 transition-transform group-open/all:rotate-180"
|
||||
/>
|
||||
</summary>
|
||||
{DOWNLOAD_GROUPS.map((group) => {
|
||||
const files = downloads.filter((download) => download.group === group);
|
||||
if (files.length === 0) return null;
|
||||
return (
|
||||
<div key={group} className="border-t border-line">
|
||||
<p className="hud-label bg-bg-sunken px-4 py-2 text-faint">{group}</p>
|
||||
<ul>
|
||||
{files.map((download) => (
|
||||
<li key={download.href} className="border-t border-line">
|
||||
<a
|
||||
href={download.href}
|
||||
className="group/file flex items-center justify-between gap-4 px-4 py-3 hover:bg-bg-sunken"
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm text-ink">{download.label}</span>
|
||||
<span className="block text-sm text-muted">{download.description}</span>
|
||||
<span className="mt-1 block truncate font-mono text-[11px] text-faint">
|
||||
{download.fileName}
|
||||
</span>
|
||||
</span>
|
||||
<ArrowDownToLineIcon
|
||||
aria-hidden
|
||||
className="h-4 w-4 shrink-0 text-muted group-hover/file:text-ink"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
function ExternalLink({ href, children }: { href: string; children: ReactNode }) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hud-label flex w-fit items-center gap-1.5 text-muted hover:text-ink"
|
||||
>
|
||||
{children}
|
||||
<ArrowUpRightIcon aria-hidden className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppFamily({ app, index }: { app: CraftApp; index: string }) {
|
||||
const siblings = CRAFTING_APPS.filter((other) => other.slug !== app.slug);
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import type { CraftAppSlug } from "./crafting-apps";
|
||||
|
||||
// GitHub releases for the Crafting Apps, which drive every download button on
|
||||
// the /apps pages.
|
||||
//
|
||||
// To bump every app: change CRAFTING_APPS_LATEST_VERSION.
|
||||
// To pin one app to another version: give it its own release in
|
||||
// CRAFT_APP_RELEASES, e.g. `craftRelease("0.1.1")`.
|
||||
// To hide an app's downloads (Discord waitlist instead): set it to null.
|
||||
// If a release renames, adds or drops files, edit CRAFT_RELEASE_ASSETS, or
|
||||
// pass a custom list as craftRelease's second argument for a single app.
|
||||
//
|
||||
// Release tags are `v<version>` and live at github.com/storytold/<slug>.
|
||||
|
||||
export const CRAFTING_APPS_LATEST_VERSION = "0.2.0";
|
||||
|
||||
export type CraftDesktopPlatform = "macOS" | "Windows" | "Linux";
|
||||
|
||||
/** Order of the per-platform download buttons. */
|
||||
export const CRAFT_DESKTOP_PLATFORMS: CraftDesktopPlatform[] = ["macOS", "Windows", "Linux"];
|
||||
|
||||
export type CraftReleaseAsset = {
|
||||
/** Asset filename; `{slug}` and `{version}` are filled in per app. */
|
||||
file: string;
|
||||
/** Group in the full file list. "Other" holds the CLI, web build and checksums. */
|
||||
group: CraftDesktopPlatform | "Other";
|
||||
/** Short name, e.g. "Installer (x64)". */
|
||||
label: string;
|
||||
/** One line on who should pick this file. */
|
||||
description: string;
|
||||
/** CPU architecture, used to choose between recommended Linux builds. */
|
||||
arch?: "universal" | "x64" | "x86" | "x86_64" | "aarch64";
|
||||
/**
|
||||
* Offered as a platform's main download button. The first recommended
|
||||
* asset in a group is the default; a later one wins only when its `arch`
|
||||
* matches the visitor's detected CPU.
|
||||
*/
|
||||
recommended?: boolean;
|
||||
};
|
||||
|
||||
export type CraftRelease = {
|
||||
/** Version without the leading "v", e.g. "0.2.0". */
|
||||
version: string;
|
||||
/** Git tag. Defaults to `v<version>`. */
|
||||
tag?: string;
|
||||
assets: readonly CraftReleaseAsset[];
|
||||
};
|
||||
|
||||
// The file set every Crafting App publishes, in display order within each group.
|
||||
export const CRAFT_RELEASE_ASSETS: readonly CraftReleaseAsset[] = [
|
||||
{
|
||||
file: "{slug}-{version}-macos-universal.dmg",
|
||||
group: "macOS",
|
||||
label: "Disk image (Universal)",
|
||||
description: "Apple silicon and Intel Macs.",
|
||||
arch: "universal",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
file: "{slug}-cli-{version}-macos-universal.zip",
|
||||
group: "macOS",
|
||||
label: "Command-line tool (Universal)",
|
||||
description: "The headless CLI for scripts and agents.",
|
||||
arch: "universal",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-windows-x64.msi",
|
||||
group: "Windows",
|
||||
label: "Installer (64-bit)",
|
||||
description: "Most Windows 10 and 11 PCs.",
|
||||
arch: "x64",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-windows-x64-portable.zip",
|
||||
group: "Windows",
|
||||
label: "Portable (64-bit)",
|
||||
description: "Unzip and run, no install.",
|
||||
arch: "x64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-windows-x86.msi",
|
||||
group: "Windows",
|
||||
label: "Installer (32-bit)",
|
||||
description: "Older 32-bit Windows.",
|
||||
arch: "x86",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-windows-x86-portable.zip",
|
||||
group: "Windows",
|
||||
label: "Portable (32-bit)",
|
||||
description: "Unzip and run on 32-bit Windows.",
|
||||
arch: "x86",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-x86_64.AppImage",
|
||||
group: "Linux",
|
||||
label: "AppImage (x86_64)",
|
||||
description: "Any distro. Mark it executable and run.",
|
||||
arch: "x86_64",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-x86_64.deb",
|
||||
group: "Linux",
|
||||
label: "Debian package (x86_64)",
|
||||
description: "Debian, Ubuntu and derivatives.",
|
||||
arch: "x86_64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-x86_64.rpm",
|
||||
group: "Linux",
|
||||
label: "RPM package (x86_64)",
|
||||
description: "Fedora, RHEL and openSUSE.",
|
||||
arch: "x86_64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-x86_64.tar.gz",
|
||||
group: "Linux",
|
||||
label: "Tarball (x86_64)",
|
||||
description: "Plain binaries to unpack anywhere.",
|
||||
arch: "x86_64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-aarch64.AppImage",
|
||||
group: "Linux",
|
||||
label: "AppImage (ARM64)",
|
||||
description: "Any distro on 64-bit ARM.",
|
||||
arch: "aarch64",
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-aarch64.deb",
|
||||
group: "Linux",
|
||||
label: "Debian package (ARM64)",
|
||||
description: "Debian, Ubuntu and derivatives on ARM.",
|
||||
arch: "aarch64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-aarch64.rpm",
|
||||
group: "Linux",
|
||||
label: "RPM package (ARM64)",
|
||||
description: "Fedora, RHEL and openSUSE on ARM.",
|
||||
arch: "aarch64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-{version}-linux-aarch64.tar.gz",
|
||||
group: "Linux",
|
||||
label: "Tarball (ARM64)",
|
||||
description: "Plain binaries for 64-bit ARM.",
|
||||
arch: "aarch64",
|
||||
},
|
||||
{
|
||||
file: "{slug}-web-{version}.zip",
|
||||
group: "Other",
|
||||
label: "Web build",
|
||||
description: "The WebAssembly app, to host yourself.",
|
||||
},
|
||||
{
|
||||
file: "SHA256SUMS.txt",
|
||||
group: "Other",
|
||||
label: "SHA-256 checksums",
|
||||
description: "Verify any download above.",
|
||||
},
|
||||
];
|
||||
|
||||
export function craftRelease(
|
||||
version: string,
|
||||
assets: readonly CraftReleaseAsset[] = CRAFT_RELEASE_ASSETS,
|
||||
): CraftRelease {
|
||||
return { version, assets };
|
||||
}
|
||||
|
||||
const LATEST = craftRelease(CRAFTING_APPS_LATEST_VERSION);
|
||||
|
||||
// Every app must be listed, so a new slug fails the type check until it is.
|
||||
export const CRAFT_APP_RELEASES: Record<CraftAppSlug, CraftRelease | null> = {
|
||||
photocraft: LATEST,
|
||||
vectorcraft: LATEST,
|
||||
filmcraft: LATEST,
|
||||
lightcraft: LATEST,
|
||||
printcraft: LATEST,
|
||||
effectcraft: LATEST,
|
||||
designcraft: LATEST,
|
||||
};
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { Tip } from "./campaign-data";
|
||||
import {
|
||||
CRAFT_APP_RELEASES,
|
||||
type CraftRelease,
|
||||
type CraftReleaseAsset,
|
||||
} from "./crafting-app-releases";
|
||||
import { mediaUrl } from "./links";
|
||||
|
||||
// The Crafting Apps family: open-source native apps from the ArtCraft team,
|
||||
@@ -9,8 +14,8 @@ import { mediaUrl } from "./links";
|
||||
// Facts (counts, timings, formats) come from each repo's README — keep them
|
||||
// in sync when the READMEs change.
|
||||
//
|
||||
// Apps without a published GitHub release show a Discord waitlist. When one
|
||||
// ships, give it `release` and its page offers the installers instead.
|
||||
// Download versions and files live in crafting-app-releases.ts. Apps whose
|
||||
// release there is null show a Discord waitlist instead of installers.
|
||||
//
|
||||
// Assets live in public/images/apps/<slug>/: icon.webp (the repo's 512 px
|
||||
// hicolor render at 256 px), og.jpg (1200×630 share card) and the shots.
|
||||
@@ -37,13 +42,6 @@ export type CraftShot = {
|
||||
portrait?: boolean;
|
||||
};
|
||||
|
||||
export type CraftRelease = {
|
||||
/** Tag without the leading "v", e.g. "0.1.1-rc.4". */
|
||||
version: string;
|
||||
/** Asset names are `<slug>-<version>-<suffix>`. */
|
||||
files: { platform: Exclude<CraftPlatform, "Web">; suffix: string }[];
|
||||
};
|
||||
|
||||
export type CraftApp = {
|
||||
slug: CraftAppSlug;
|
||||
/** Prefix before "Craft" — the wordmark colors "Craft". */
|
||||
@@ -62,7 +60,12 @@ export type CraftApp = {
|
||||
features: Tip[];
|
||||
/** First shot is the hero. */
|
||||
shots: [CraftShot, ...CraftShot[]];
|
||||
release?: CraftRelease;
|
||||
};
|
||||
|
||||
/** One release file, resolved to a URL for a specific app. */
|
||||
export type CraftDownload = Omit<CraftReleaseAsset, "file"> & {
|
||||
fileName: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export const CRAFTING_APPS_GITHUB_ORG = "https://github.com/storytold";
|
||||
@@ -79,14 +82,6 @@ export const CRAFTING_APPS: CraftApp[] = [
|
||||
schemaCategory: "DesignApplication",
|
||||
platforms: ["macOS", "Windows", "Linux", "Web"],
|
||||
rustVersion: "1.90",
|
||||
release: {
|
||||
version: "0.1.1-rc.4",
|
||||
files: [
|
||||
{ platform: "macOS", suffix: "macos-universal.dmg" },
|
||||
{ platform: "Windows", suffix: "windows-x64.msi" },
|
||||
{ platform: "Linux", suffix: "linux-x86_64.AppImage" },
|
||||
],
|
||||
},
|
||||
features: [
|
||||
{
|
||||
title: "Familiar by design",
|
||||
@@ -630,19 +625,34 @@ export function craftAppIconSourceUrl(app: CraftApp): string {
|
||||
return `https://raw.githubusercontent.com/storytold/${app.slug}/main/assets/app-icon/${app.slug}-1024.png`;
|
||||
}
|
||||
|
||||
export function craftReleasePageUrl(app: CraftApp, release: CraftRelease): string {
|
||||
return `${craftAppRepo(app)}/releases/tag/v${release.version}`;
|
||||
export function craftAppRelease(app: CraftApp): CraftRelease | null {
|
||||
return CRAFT_APP_RELEASES[app.slug];
|
||||
}
|
||||
|
||||
export function craftReleaseTag(release: CraftRelease): string {
|
||||
return release.tag ?? `v${release.version}`;
|
||||
}
|
||||
|
||||
export function craftReleasePageUrl(app: CraftApp, release: CraftRelease): string {
|
||||
return `${craftAppRepo(app)}/releases/tag/${craftReleaseTag(release)}`;
|
||||
}
|
||||
|
||||
export function craftReleasesUrl(app: CraftApp): string {
|
||||
return `${craftAppRepo(app)}/releases`;
|
||||
}
|
||||
|
||||
/** Every file in the release, in catalog order. */
|
||||
export function craftReleaseDownloads(
|
||||
app: CraftApp,
|
||||
release: CraftRelease,
|
||||
): { platform: string; href: string }[] {
|
||||
const base = `${craftAppRepo(app)}/releases/download/v${release.version}`;
|
||||
return release.files.map(({ platform, suffix }) => ({
|
||||
platform,
|
||||
href: `${base}/${app.slug}-${release.version}-${suffix}`,
|
||||
}));
|
||||
): CraftDownload[] {
|
||||
const base = `${craftAppRepo(app)}/releases/download/${craftReleaseTag(release)}`;
|
||||
return release.assets.map(({ file, ...asset }) => {
|
||||
const fileName = file
|
||||
.replaceAll("{slug}", app.slug)
|
||||
.replaceAll("{version}", release.version);
|
||||
return { ...asset, fileName, href: `${base}/${fileName}` };
|
||||
});
|
||||
}
|
||||
|
||||
export function craftAppOgImage(app: CraftApp): string {
|
||||
|
||||
Reference in new issue
Block a user