diff --git a/app/Http/Controllers/DownloadController.php b/app/Http/Controllers/DownloadController.php
index ff9fa860..d81073a5 100644
--- a/app/Http/Controllers/DownloadController.php
+++ b/app/Http/Controllers/DownloadController.php
@@ -18,14 +18,13 @@
* The download page, `/download` and `/vi/download` (sitemap §A.1, §E.5).
*
* Everything the page links to outside this site arrives here, as props: the
- * DMG URLs and sizes from the release service, the App Store and GitHub
+ * DMG URL and size from the release service, the App Store and GitHub
* Releases URLs from `platforms.json`, and the docs, source and licence links
* from `facts.json`. No URL is typed into the page or its copy, so the
* external-URL guard in `Data/FactsDataTest` holds by construction.
*
- * Both DMG links are in the server-rendered HTML. The browser only ever
- * highlights one of them (resources/js/lib/device.ts); nothing starts a
- * download on its own.
+ * The DMG link is in the server-rendered HTML; nothing starts a download on
+ * its own.
*/
class DownloadController extends Controller
{
diff --git a/app/Services/Releases/MacRelease.php b/app/Services/Releases/MacRelease.php
index 027aac15..4bd0eaac 100644
--- a/app/Services/Releases/MacRelease.php
+++ b/app/Services/Releases/MacRelease.php
@@ -10,8 +10,8 @@
* `source` says where: `github` (the releases API, with file sizes and
* SHA-256 digests), `appcast` (the Sparkle feed, with neither) or
* `unavailable` (neither answered and there is no earlier good copy). An
- * unavailable release has no version, and both of its asset URLs are GitHub's
- * "latest release" page, so a button still leads somewhere real while the page
+ * unavailable release has no version, and its asset URL is GitHub's "latest
+ * release" page, so the button still leads somewhere real while the page
* states no version it cannot stand behind.
*
* `toArray()` is the cached, locale-neutral form. `toProps()` adds the date
@@ -19,7 +19,7 @@
* pre-formatted.
*
* @phpstan-type Asset array{name: string|null, url: string, bytes: int|null, sha256: string|null}
- * @phpstan-type Cached array{version: string|null, publishedAt: string|null, assets: array{arm64: Asset, x86_64: Asset}, releaseUrl: string, releasesUrl: string, source: string}
+ * @phpstan-type Cached array{version: string|null, publishedAt: string|null, asset: Asset, releaseUrl: string, releasesUrl: string, source: string}
*/
final readonly class MacRelease
{
@@ -32,31 +32,30 @@
/**
* @param string|null $version e.g. `0.77.0`, without the `v`
* @param string|null $publishedAt `Y-m-d`
- * @param array{arm64: array{name: string|null, url: string, bytes: int|null, sha256: string|null}, x86_64: array{name: string|null, url: string, bytes: int|null, sha256: string|null}} $assets
+ * @param array{name: string|null, url: string, bytes: int|null, sha256: string|null} $asset the universal DMG
* @param string $releaseUrl this release's page (notes and every asset), or the latest-release page
* @param string $releasesUrl every release
*/
public function __construct(
public ?string $version,
public ?string $publishedAt,
- public array $assets,
+ public array $asset,
public string $releaseUrl,
public string $releasesUrl,
public string $source,
) {}
/**
- * No release data at all: both buttons go to GitHub's latest-release page.
+ * No release data at all: the button goes to GitHub's latest-release page.
*/
public static function unavailable(string $repo, string $releasesUrl): self
{
$latest = "https://github.com/{$repo}/releases/latest";
- $asset = ['name' => null, 'url' => $latest, 'bytes' => null, 'sha256' => null];
return new self(
version: null,
publishedAt: null,
- assets: ['arm64' => $asset, 'x86_64' => $asset],
+ asset: ['name' => null, 'url' => $latest, 'bytes' => null, 'sha256' => null],
releaseUrl: $latest,
releasesUrl: $releasesUrl,
source: self::SOURCE_UNAVAILABLE,
@@ -64,39 +63,28 @@ public static function unavailable(string $repo, string $releasesUrl): self
}
/**
- * Rebuilds a cached copy, or returns null when the copy is not one.
+ * Rebuilds a cached copy, or returns null when the copy is not one. A copy
+ * stored before the universal DMG, with a DMG per architecture, is not one.
*
* @param array $data
*/
public static function fromArray(array $data): ?self
{
- $assets = $data['assets'] ?? null;
+ $asset = $data['asset'] ?? null;
- if (! is_array($assets) || ! is_string($data['releaseUrl'] ?? null) || ! is_string($data['releasesUrl'] ?? null) || ! is_string($data['source'] ?? null)) {
+ if (! is_array($asset) || ! is_string($asset['url'] ?? null) || ! is_string($data['releaseUrl'] ?? null) || ! is_string($data['releasesUrl'] ?? null) || ! is_string($data['source'] ?? null)) {
return null;
}
- $normalized = [];
-
- foreach (PlatformCatalog::MAC_ARCHITECTURES as $arch) {
- $asset = $assets[$arch] ?? null;
-
- if (! is_array($asset) || ! is_string($asset['url'] ?? null)) {
- return null;
- }
-
- $normalized[$arch] = [
+ return new self(
+ version: is_string($data['version'] ?? null) ? $data['version'] : null,
+ publishedAt: is_string($data['publishedAt'] ?? null) ? $data['publishedAt'] : null,
+ asset: [
'name' => is_string($asset['name'] ?? null) ? $asset['name'] : null,
'url' => $asset['url'],
'bytes' => is_int($asset['bytes'] ?? null) ? $asset['bytes'] : null,
'sha256' => is_string($asset['sha256'] ?? null) ? $asset['sha256'] : null,
- ];
- }
-
- return new self(
- version: is_string($data['version'] ?? null) ? $data['version'] : null,
- publishedAt: is_string($data['publishedAt'] ?? null) ? $data['publishedAt'] : null,
- assets: $normalized,
+ ],
releaseUrl: $data['releaseUrl'],
releasesUrl: $data['releasesUrl'],
source: $data['source'],
@@ -109,14 +97,14 @@ public function isAvailable(): bool
}
/**
- * @return array{version: string|null, publishedAt: string|null, assets: array{arm64: array{name: string|null, url: string, bytes: int|null, sha256: string|null}, x86_64: array{name: string|null, url: string, bytes: int|null, sha256: string|null}}, releaseUrl: string, releasesUrl: string, source: string}
+ * @return array{version: string|null, publishedAt: string|null, asset: array{name: string|null, url: string, bytes: int|null, sha256: string|null}, releaseUrl: string, releasesUrl: string, source: string}
*/
public function toArray(): array
{
return [
'version' => $this->version,
'publishedAt' => $this->publishedAt,
- 'assets' => $this->assets,
+ 'asset' => $this->asset,
'releaseUrl' => $this->releaseUrl,
'releasesUrl' => $this->releasesUrl,
'source' => $this->source,
@@ -132,7 +120,7 @@ public function toArray(): array
* (architecture §1.5). `LL` is "October 2, 2026" in English and
* "2 tháng 10 năm 2026" in Vietnamese.
*
- * @return array{version: string|null, publishedAt: string|null, publishedAtFormatted: string|null, assets: array{arm64: array{name: string|null, url: string, bytes: int|null, sha256: string|null}, x86_64: array{name: string|null, url: string, bytes: int|null, sha256: string|null}}, releaseUrl: string, releasesUrl: string, source: string}
+ * @return array{version: string|null, publishedAt: string|null, publishedAtFormatted: string|null, asset: array{name: string|null, url: string, bytes: int|null, sha256: string|null}, releaseUrl: string, releasesUrl: string, source: string}
*/
public function toProps(string $locale): array
{
diff --git a/app/Services/Releases/MacReleaseService.php b/app/Services/Releases/MacReleaseService.php
index b5555137..db4f9090 100644
--- a/app/Services/Releases/MacReleaseService.php
+++ b/app/Services/Releases/MacReleaseService.php
@@ -12,17 +12,17 @@
/**
* The current Mac release, for the download buttons (architecture §1.13).
*
- * Primary source: GitHub's `releases/latest` for the app repository. Plugin
- * releases share that repository but are published with `--latest=false`
- * (`build-plugin.yml:325`), so "latest" is the app; a release is still
- * accepted only when it is neither a draft nor a prerelease and carries both
- * DMGs, named for its own version. That rejects a plugin release that slipped
- * through, and an app release caught halfway through its upload.
+ * Primary source: GitHub's `releases/latest` for the app repository. Older
+ * plugin releases share that repository but were published with
+ * `--latest=false`, so "latest" is the app; a release is still accepted only
+ * when it is neither a draft nor a prerelease and carries the DMG, named for
+ * its own version. That rejects a plugin release that slipped through, and an
+ * app release caught halfway through its upload.
*
- * Fallback: the top item of the Sparkle appcast, with the DMG names and URLs
- * built from `platforms.json` (`assetTemplate`, and the `dmg` destination's
- * `urlTemplate`). Never the first item of the full releases list, which plugin
- * releases crowd.
+ * Fallback: the current stable item of the Sparkle appcast, with the DMG name
+ * and URL built from `platforms.json` (`assetTemplate`, and the `dmg`
+ * destination's `urlTemplate`). Never the first item of the full releases
+ * list, which plugin releases crowd.
*
* **A request never waits for GitHub.** `release:refresh`, scheduled every 15
* minutes (routes/console.php), calls the sources; a page only reads what it
@@ -40,7 +40,7 @@
* good copy keeps being served.
* - When `releases:mac` is missing (after `cache:clear`, or if the scheduler
* has stopped), a request serves the last good copy, or `unavailable` if
- * there never was one (both buttons open GitHub's latest-release page and no
+ * there never was one (the button opens GitHub's latest-release page and no
* version is shown), and asks for one refresh after its response has been
* sent (`defer()`): never while the failure marker stands, and under a lock,
* so concurrent requests start one refresh between them.
@@ -227,16 +227,10 @@ public function fromGitHub(): ?MacRelease
return null;
}
- $assets = [];
+ $asset = $this->findAsset($release['assets'] ?? [], $this->platforms->macAssetName($version));
- foreach (PlatformCatalog::MAC_ARCHITECTURES as $arch) {
- $asset = $this->findAsset($release['assets'] ?? [], $this->platforms->macAssetName($arch, $version));
-
- if ($asset === null) {
- return null;
- }
-
- $assets[$arch] = $asset;
+ if ($asset === null) {
+ return null;
}
$htmlUrl = $release['html_url'] ?? null;
@@ -244,7 +238,7 @@ public function fromGitHub(): ?MacRelease
return new MacRelease(
version: $version,
publishedAt: $this->date($release['published_at'] ?? null),
- assets: $assets,
+ asset: $asset,
releaseUrl: is_string($htmlUrl) && str_starts_with($htmlUrl, 'https://') ? $htmlUrl : $this->tagUrl($version),
releasesUrl: $this->releasesUrl(),
source: MacRelease::SOURCE_GITHUB,
@@ -252,9 +246,9 @@ public function fromGitHub(): ?MacRelease
}
/**
- * The release from the Sparkle appcast's top item, or null. Sizes and
- * checksums are not in the feed (its enclosures are the update zips), so
- * they stay null.
+ * The release from the Sparkle appcast's current stable item, or null.
+ * Sizes and checksums are not in the feed (its enclosures are the update
+ * archives), so they stay null.
*/
public function fromAppcast(): ?MacRelease
{
@@ -265,24 +259,18 @@ public function fromAppcast(): ?MacRelease
}
$version = $item['version'];
- $assets = [];
-
- foreach (PlatformCatalog::MAC_ARCHITECTURES as $arch) {
- $name = $this->platforms->macAssetName($arch, $version);
+ $name = $this->platforms->macAssetName($version);
- $assets[$arch] = [
+ return new MacRelease(
+ version: $version,
+ publishedAt: $item['publishedAt'],
+ asset: [
'name' => $name,
- 'url' => $this->platforms->macAssetUrl($arch, $version)
+ 'url' => $this->platforms->macAssetUrl($version)
?? 'https://github.com/' . $this->repo() . "/releases/download/v{$version}/" . rawurlencode($name),
'bytes' => null,
'sha256' => null,
- ];
- }
-
- return new MacRelease(
- version: $version,
- publishedAt: $item['publishedAt'],
- assets: $assets,
+ ],
releaseUrl: $this->tagUrl($version),
releasesUrl: $this->releasesUrl(),
source: MacRelease::SOURCE_APPCAST,
diff --git a/app/Services/Releases/PlatformCatalog.php b/app/Services/Releases/PlatformCatalog.php
index 12cafdfe..cfe3647c 100644
--- a/app/Services/Releases/PlatformCatalog.php
+++ b/app/Services/Releases/PlatformCatalog.php
@@ -11,8 +11,8 @@
* The file is edited by hand (architecture §1.8); this class only reads it,
* for the release service, `release:check` and the download page. It never
* types a fact of its own except the DMG naming pattern, which is a fallback
- * for a file that names none: `TablePro-{version}-{arch}.dmg` has held from
- * 0.73 to 0.77 and is enforced by the app's release job (`build.yml:254-270`).
+ * for a file that names none: since 0.82.0 the app's release workflow uploads
+ * one universal `TablePro-{version}.dmg`.
*
* A missing or unreadable file reads as empty rather than throwing, so a
* broken data file degrades the download page to its GitHub fallback instead
@@ -20,16 +20,11 @@
*
* @phpstan-type Requirements array{systems: list, minVersion: string, displayVersion: string, releaseName: string|null}
* @phpstan-type Destination array{kind: string, url?: string, command?: string}
- * @phpstan-type Platform array{id: string, status: string, deviceNames?: list, requirements?: Requirements, architectures?: list, destinations?: list, release?: array{version?: string, build?: string, publishedAt?: string}, floorVersion?: string, price?: array{amount?: int|float, inAppPurchases?: bool}, storefrontExclusions?: list}
+ * @phpstan-type Platform array{id: string, status: string, deviceNames?: list, requirements?: Requirements, architectures?: list, universalBinary?: bool, assetTemplate?: string, destinations?: list, release?: array{version?: string, build?: string, publishedAt?: string}, floorVersion?: string, price?: array{amount?: int|float, inAppPurchases?: bool}, storefrontExclusions?: list}
*/
class PlatformCatalog
{
- /**
- * The two architectures the Mac app ships as separate builds, in display order.
- */
- public const MAC_ARCHITECTURES = ['arm64', 'x86_64'];
-
- private const DEFAULT_ASSET_TEMPLATE = 'TablePro-{version}-{arch}.dmg';
+ private const DEFAULT_ASSET_TEMPLATE = 'TablePro-{version}.dmg';
/**
* @var list>|null
@@ -127,26 +122,20 @@ public function destination(string $id, string $kind): ?array
}
/**
- * The DMG file name for one Mac architecture and version.
+ * The DMG file name for one Mac version.
*/
- public function macAssetName(string $arch, string $version): string
+ public function macAssetName(string $version): string
{
- $template = self::DEFAULT_ASSET_TEMPLATE;
-
- foreach ($this->find('mac')['architectures'] ?? [] as $architecture) {
- if (is_array($architecture) && ($architecture['id'] ?? null) === $arch && is_string($architecture['assetTemplate'] ?? null)) {
- $template = $architecture['assetTemplate'];
- }
- }
+ $template = $this->find('mac')['assetTemplate'] ?? null;
- return strtr($template, ['{version}' => $version, '{arch}' => $arch]);
+ return strtr(is_string($template) && $template !== '' ? $template : self::DEFAULT_ASSET_TEMPLATE, ['{version}' => $version]);
}
/**
- * The DMG's download URL for one Mac architecture and version, from the
- * `dmg` destination's `urlTemplate`, or null when the file names none.
+ * The DMG's download URL for one Mac version, from the `dmg` destination's
+ * `urlTemplate`, or null when the file names none.
*/
- public function macAssetUrl(string $arch, string $version): ?string
+ public function macAssetUrl(string $version): ?string
{
$template = $this->destination('mac', 'dmg')['urlTemplate'] ?? null;
@@ -154,7 +143,7 @@ public function macAssetUrl(string $arch, string $version): ?string
return null;
}
- return strtr($template, ['{version}' => $version, '{arch}' => $arch]);
+ return strtr($template, ['{version}' => $version]);
}
/**
diff --git a/app/Services/Releases/SparkleAppcast.php b/app/Services/Releases/SparkleAppcast.php
index 64d63d47..d95b86cd 100644
--- a/app/Services/Releases/SparkleAppcast.php
+++ b/app/Services/Releases/SparkleAppcast.php
@@ -7,31 +7,34 @@
use SimpleXMLElement;
/**
- * The Sparkle feed the installed Mac app updates from.
+ * The Sparkle feed the installed Mac app updates from, at `updates.tablepro.app`.
*
- * Read as the fallback when the GitHub releases API fails. It is served from
- * `raw.githubusercontent.com`, which is not subject to the API's
- * unauthenticated limit of 60 calls an hour, and the release job writes it only
- * after the release's assets are uploaded (`build.yml:330-405`), so a version
- * here always has its DMGs.
+ * Read as the fallback when the GitHub releases API fails. It is a GitHub
+ * Pages site, which is not subject to the API's unauthenticated limit of 60
+ * calls an hour, and the app's release workflow writes an item only after the
+ * release's assets are uploaded, so a version here always has its DMG.
*
- * Only the top item is read. Items come in per-architecture pairs for the same
- * version, newest first; the feed carries the update zips, never the DMGs, so
- * the DMG names come from `platforms.json` rather than from the enclosure.
+ * Stable and beta share the feed; a beta item carries `sparkle:channel`. The
+ * current release is the item with no channel and the highest
+ * `sparkle:version` (the build number), wherever it sits in the feed. The
+ * enclosures are the update archives, never the DMG, so the DMG name comes
+ * from `platforms.json` rather than from the feed.
*/
class SparkleAppcast
{
+ public const FEED_URL = 'https://updates.tablepro.app/appcast.xml';
+
private const SPARKLE_NAMESPACE = 'http://www.andymatuschak.org/xml-namespaces/sparkle';
private const TIMEOUT_SECONDS = 5;
public function feedUrl(): string
{
- return 'https://raw.githubusercontent.com/' . config('services.github.repo') . '/main/appcast.xml';
+ return self::FEED_URL;
}
/**
- * The newest item of the live feed, or null when it cannot be read.
+ * The current stable item of the live feed, or null when it cannot be read.
*
* @return array{version: string, publishedAt: string|null, minimumSystemVersion: string|null}|null
*/
@@ -51,7 +54,8 @@ public function latest(): ?array
}
/**
- * The top item of a feed document, or null when it holds no usable item.
+ * The stable item with the highest build in a feed document, or null when
+ * it holds none.
*
* @return array{version: string, publishedAt: string|null, minimumSystemVersion: string|null}|null
*/
@@ -70,30 +74,41 @@ public static function parse(string $xml): ?array
libxml_use_internal_errors($previous);
}
- if ($document === false || ! isset($document->channel->item[0])) {
+ if ($document === false || ! isset($document->channel->item)) {
return null;
}
- $item = $document->channel->item[0];
- $sparkle = $item->children(self::SPARKLE_NAMESPACE);
+ $latest = null;
+ $latestBuild = -1;
- $version = trim((string) $sparkle->shortVersionString);
+ foreach ($document->channel->item as $item) {
+ $sparkle = $item->children(self::SPARKLE_NAMESPACE);
- if ($version === '') {
- $version = trim((string) $item->title);
- }
+ if (trim((string) $sparkle->channel) !== '') {
+ continue;
+ }
- if (preg_match('/^\d+(\.\d+)*$/', $version) !== 1) {
- return null;
- }
+ $build = trim((string) $sparkle->version);
+ $version = trim((string) $sparkle->shortVersionString);
- $minimum = trim((string) $sparkle->minimumSystemVersion);
+ if ($version === '') {
+ $version = trim((string) $item->title);
+ }
+
+ if (preg_match('/^\d+$/', $build) !== 1 || preg_match('/^\d+(\.\d+)*$/', $version) !== 1 || (int) $build <= $latestBuild) {
+ continue;
+ }
+
+ $minimum = trim((string) $sparkle->minimumSystemVersion);
+ $latestBuild = (int) $build;
+ $latest = [
+ 'version' => $version,
+ 'publishedAt' => self::date(trim((string) $item->pubDate)),
+ 'minimumSystemVersion' => $minimum !== '' ? $minimum : null,
+ ];
+ }
- return [
- 'version' => $version,
- 'publishedAt' => self::date(trim((string) $item->pubDate)),
- 'minimumSystemVersion' => $minimum !== '' ? $minimum : null,
- ];
+ return $latest;
}
/**
diff --git a/resources/data/content/de/download.json b/resources/data/content/de/download.json
index e27fb4c9..dfbf1330 100644
--- a/resources/data/content/de/download.json
+++ b/resources/data/content/de/download.json
@@ -1,7 +1,7 @@
{
"seo": {
"title": "TablePro herunterladen",
- "description": "Lade TablePro mit separaten Builds für Apple silicon und Intel herunter oder installiere es mit Homebrew. Die App für iPhone und iPad gibt es im App Store."
+ "description": "Lade TablePro für Macs mit Apple silicon und Intel herunter oder installiere es mit Homebrew. Die App für iPhone und iPad gibt es im App Store."
},
"og": {
"kicker": "Download",
@@ -33,7 +33,7 @@
"Öffne TablePro aus Programme. Wähle Open, wenn macOS um Bestätigung bittet.",
"Füge eine Verbindung hinzu oder teste die enthaltene Chinook-SQLite-Datenbank über Help > Open Sample Database."
],
- "signing": "Beide Versionen sind mit einer Developer ID signiert und von Apple notarisiert. Du musst daher keine Sicherheitseinstellung ändern, um TablePro zu öffnen.",
+ "signing": "TablePro ist mit einer Developer ID signiert und von Apple notarisiert. Du musst daher keine Sicherheitseinstellung ändern, um TablePro zu öffnen.",
"drivers": "Gängige Treiber sind enthalten. Andere benötigen Internet für den ersten Download und werden unabhängig von der App aktualisiert.",
"databases": "Unterstützte Datenbanken",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "Ältere Versionen und Versionshinweise",
- "body": "Jede Version mit ihren Hinweisen und beiden Disk-Images findest du auf GitHub Releases. Das Änderungsprotokoll listet die Änderungen jeder Version auf.",
+ "body": "Jede Version mit ihren Hinweisen und Disk-Images findest du auf GitHub Releases. Das Änderungsprotokoll listet die Änderungen jeder Version auf.",
"releases": "Alle Versionen auf GitHub",
"changelog": "Änderungsprotokoll (Englisch)"
}
diff --git a/resources/data/content/de/faq.json b/resources/data/content/de/faq.json
index 417dacd5..d5fbd8b3 100644
--- a/resources/data/content/de/faq.json
+++ b/resources/data/content/de/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "Auf welchen Macs läuft TablePro?",
"answer": [
- "Die Mac-App benötigt {macRequirement}. Wähle Apple silicon oder Intel auf der Downloadseite oder lass Homebrew den Build auswählen."
+ "Die Mac-App benötigt {macRequirement} und läuft nativ auf Apple silicon und Intel. Lade sie von der Downloadseite oder installiere sie mit Homebrew."
]
},
{
diff --git a/resources/data/content/en/download.json b/resources/data/content/en/download.json
index 1346e9c2..e23f265c 100644
--- a/resources/data/content/en/download.json
+++ b/resources/data/content/en/download.json
@@ -1,7 +1,7 @@
{
"seo": {
"title": "Download TablePro",
- "description": "Download TablePro, with separate builds for Apple silicon and Intel, or install it with Homebrew. The iPhone and iPad app is on the App Store."
+ "description": "Download TablePro for Apple silicon and Intel Macs, or install it with Homebrew. The iPhone and iPad app is on the App Store."
},
"og": {
"kicker": "Download",
@@ -33,7 +33,7 @@
"Open TablePro from Applications. Choose Open when macOS asks you to confirm.",
"Add a connection, or try the bundled Chinook SQLite database through Help > Open Sample Database."
],
- "signing": "Both builds are signed with a Developer ID and notarized by Apple, so you don’t need to change any security setting to open TablePro.",
+ "signing": "TablePro is signed with a Developer ID and notarized by Apple, so you don’t need to change any security setting to open it.",
"drivers": "Common drivers are bundled. Others need an internet connection to download on first use and update separately from the app.",
"databases": "Supported databases",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "Older versions and release notes",
- "body": "Every release, with its notes and both disk images, is on GitHub Releases. The changelog lists what changed in each version.",
+ "body": "Every release, with its notes and disk images, is on GitHub Releases. The changelog lists what changed in each version.",
"releases": "All releases on GitHub",
"changelog": "Changelog"
}
diff --git a/resources/data/content/en/faq.json b/resources/data/content/en/faq.json
index fb2bfdff..0dc2817a 100644
--- a/resources/data/content/en/faq.json
+++ b/resources/data/content/en/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "Which Macs does TablePro run on?",
"answer": [
- "The Mac app requires {macRequirement}. Choose Apple silicon or Intel on the download page, or let Homebrew select the build."
+ "The Mac app requires {macRequirement} and runs natively on Apple silicon and Intel. Get it from the download page or with Homebrew."
]
},
{
diff --git a/resources/data/content/es/download.json b/resources/data/content/es/download.json
index f2dbb341..249d6c44 100644
--- a/resources/data/content/es/download.json
+++ b/resources/data/content/es/download.json
@@ -1,7 +1,7 @@
{
"seo": {
"title": "Descargar TablePro",
- "description": "Descarga TablePro, con versiones separadas para Apple silicon e Intel, o instálalo con Homebrew. La app para iPhone y iPad está en la App Store."
+ "description": "Descarga TablePro para Mac con Apple silicon e Intel, o instálalo con Homebrew. La app para iPhone y iPad está en la App Store."
},
"og": {
"kicker": "Descargar",
@@ -33,7 +33,7 @@
"Abre TablePro desde Aplicaciones. Elige Open cuando macOS pida confirmación.",
"Añade una conexión o prueba la base SQLite Chinook incluida desde Help > Open Sample Database."
],
- "signing": "Ambas versiones están firmadas con un Developer ID y notarizadas por Apple, así que no necesitas cambiar ningún ajuste de seguridad para abrir TablePro.",
+ "signing": "TablePro está firmado con un Developer ID y notarizado por Apple, así que no necesitas cambiar ningún ajuste de seguridad para abrirlo.",
"drivers": "Los controladores habituales vienen incluidos. Los demás necesitan internet para la primera descarga y se actualizan por separado de la app.",
"databases": "Bases de datos compatibles",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "Versiones anteriores y notas de versión",
- "body": "Todas las versiones, con sus notas y ambas imágenes de disco, están en GitHub Releases. El historial de cambios enumera las novedades de cada versión.",
+ "body": "Todas las versiones, con sus notas e imágenes de disco, están en GitHub Releases. El historial de cambios enumera las novedades de cada versión.",
"releases": "Todas las versiones en GitHub",
"changelog": "Historial de cambios (inglés)"
}
diff --git a/resources/data/content/es/faq.json b/resources/data/content/es/faq.json
index ac42b40f..5d962ad9 100644
--- a/resources/data/content/es/faq.json
+++ b/resources/data/content/es/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "¿En qué Mac funciona TablePro?",
"answer": [
- "La app para Mac necesita {macRequirement}. Elige Apple silicon o Intel en la página de descarga, o deja que Homebrew elija la versión."
+ "La app para Mac necesita {macRequirement} y funciona de forma nativa en Apple silicon e Intel. Descárgala desde la página de descarga o instálala con Homebrew."
]
},
{
diff --git a/resources/data/content/fr/download.json b/resources/data/content/fr/download.json
index 0b7b4f41..d9cf9cfa 100644
--- a/resources/data/content/fr/download.json
+++ b/resources/data/content/fr/download.json
@@ -1,7 +1,7 @@
{
"seo": {
"title": "Télécharger TablePro",
- "description": "Téléchargez TablePro en version Apple silicon ou Intel, ou installez-le avec Homebrew. L’app iPhone et iPad est sur l’App Store."
+ "description": "Téléchargez TablePro pour les Mac Apple silicon et Intel, ou installez-le avec Homebrew. L’app iPhone et iPad est sur l’App Store."
},
"og": {
"kicker": "Télécharger",
@@ -33,7 +33,7 @@
"Ouvrez TablePro depuis Applications. Choisissez Ouvrir (Open) lorsque macOS demande confirmation.",
"Ajoutez une connexion ou essayez la base SQLite Chinook incluse via Aide > Ouvrir la base de données d’exemple (Help > Open Sample Database)."
],
- "signing": "Les deux versions sont signées avec un Developer ID et notariées par Apple. Vous n’avez donc aucun réglage de sécurité à modifier pour ouvrir TablePro.",
+ "signing": "TablePro est signé avec un Developer ID et notarié par Apple. Vous n’avez donc aucun réglage de sécurité à modifier pour l’ouvrir.",
"drivers": "Les pilotes courants sont inclus. Les autres nécessitent internet pour le premier téléchargement et se mettent à jour séparément de l’app.",
"databases": "Bases de données prises en charge",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "Anciennes versions et notes de version",
- "body": "Chaque version, avec ses notes et ses deux images disque, figure sur GitHub Releases. Le journal des modifications liste les changements de chaque version.",
+ "body": "Chaque version, avec ses notes et ses images disque, figure sur GitHub Releases. Le journal des modifications liste les changements de chaque version.",
"releases": "Toutes les versions sur GitHub",
"changelog": "Journal des modifications (anglais)"
}
diff --git a/resources/data/content/fr/faq.json b/resources/data/content/fr/faq.json
index 4f239506..d936a66c 100644
--- a/resources/data/content/fr/faq.json
+++ b/resources/data/content/fr/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "Sur quels Mac TablePro fonctionne-t-il ?",
"answer": [
- "L’app Mac nécessite {macRequirement}. Choisissez Apple silicon ou Intel sur la page de téléchargement, ou laissez Homebrew sélectionner la version."
+ "L’app Mac nécessite {macRequirement} et fonctionne nativement sur Apple silicon et Intel. Téléchargez-la sur la page de téléchargement ou installez-la avec Homebrew."
]
},
{
diff --git a/resources/data/content/id/download.json b/resources/data/content/id/download.json
index f0c37444..d6996966 100644
--- a/resources/data/content/id/download.json
+++ b/resources/data/content/id/download.json
@@ -1,7 +1,7 @@
{
"seo": {
"title": "Unduh TablePro",
- "description": "Unduh TablePro, dengan versi terpisah untuk Apple silicon dan Intel, atau instal dengan Homebrew. Aplikasi iPhone dan iPad ada di App Store."
+ "description": "Unduh TablePro untuk Mac Apple silicon dan Intel, atau instal dengan Homebrew. Aplikasi iPhone dan iPad ada di App Store."
},
"og": {
"kicker": "Unduh",
@@ -33,7 +33,7 @@
"Buka TablePro dari Aplikasi. Pilih Open ketika macOS meminta Anda untuk mengonfirmasi.",
"Tambahkan koneksi, atau coba paket database Chinook SQLite melalui Help > Open Sample Database."
],
- "signing": "Kedua build ditandatangani dengan Developer ID dan dinotarisasi Apple, sehingga Anda tidak perlu mengubah pengaturan keamanan untuk membuka TablePro.",
+ "signing": "TablePro ditandatangani dengan Developer ID dan dinotarisasi Apple, sehingga Anda tidak perlu mengubah pengaturan keamanan untuk membukanya.",
"drivers": "Driver umum dibundel. Yang lain memerlukan koneksi internet untuk mengunduh pada penggunaan pertama dan memperbarui secara terpisah dari aplikasi.",
"databases": "Database yang didukung",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "Versi lama dan catatan rilis",
- "body": "Setiap rilis beserta catatan dan kedua disk image tersedia di GitHub Releases. Catatan perubahan mencantumkan perubahan dalam setiap versi.",
+ "body": "Setiap rilis beserta catatan dan disk image-nya tersedia di GitHub Releases. Catatan perubahan mencantumkan perubahan dalam setiap versi.",
"releases": "Semua rilis di GitHub",
"changelog": "Catatan perubahan (bahasa Inggris)"
}
diff --git a/resources/data/content/id/faq.json b/resources/data/content/id/faq.json
index 6c02b960..7f8d8518 100644
--- a/resources/data/content/id/faq.json
+++ b/resources/data/content/id/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "Mac apa yang dapat menjalankan TablePro?",
"answer": [
- "App Mac memerlukan {macRequirement}. Pilih Apple silicon atau Intel di halaman unduhan, atau biarkan Homebrew memilih build."
+ "App Mac memerlukan {macRequirement} dan berjalan native di Apple silicon dan Intel. Unduh dari halaman unduhan atau instal dengan Homebrew."
]
},
{
diff --git a/resources/data/content/it/download.json b/resources/data/content/it/download.json
index 5f4b4207..72ef8042 100644
--- a/resources/data/content/it/download.json
+++ b/resources/data/content/it/download.json
@@ -1,7 +1,7 @@
{
"seo": {
"title": "Scarica TablePro",
- "description": "Scarica TablePro, con build separate per Apple silicon e Intel, oppure installalo con Homebrew. L'app per iPhone e iPad si trova sullo App Store."
+ "description": "Scarica TablePro per Mac con Apple silicon e Intel, oppure installalo con Homebrew. L'app per iPhone e iPad si trova sullo App Store."
},
"og": {
"kicker": "Download",
@@ -33,7 +33,7 @@
"Apri TablePro da Applicazioni. Scegli Open quando macOS ti chiede di confermare.",
"Aggiungi una connessione o prova il database Chinook SQLite in bundle tramite Help > Open Sample Database."
],
- "signing": "Entrambe le build sono firmate con un Developer ID e autenticate da Apple, quindi non devi modificare alcuna imposto di sicurezza per aprire TablePro.",
+ "signing": "TablePro è firmato con un Developer ID e autenticato da Apple, quindi non devi modificare alcuna impostazione di sicurezza per aprirlo.",
"drivers": "I driver comuni sono raggruppati. Altri necessitano di una connessione Internet per scaricare al primo utilizzo e aggiornare separatamente dall'app.",
"databases": "Database supportati",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "Versioni precedenti e note di rilascio",
- "body": "Ogni rilascio, con le note e le due immagini disco, si trova su GitHub Releases. Il registro delle modifiche elenca le novità di ogni versione.",
+ "body": "Ogni rilascio, con le note e le immagini disco, si trova su GitHub Releases. Il registro delle modifiche elenca le novità di ogni versione.",
"releases": "Tutti i rilasci su GitHub",
"changelog": "Registro delle modifiche (inglese)"
}
diff --git a/resources/data/content/it/faq.json b/resources/data/content/it/faq.json
index bc0383d3..e955bc3d 100644
--- a/resources/data/content/it/faq.json
+++ b/resources/data/content/it/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "Su quali Mac funziona TablePro?",
"answer": [
- "L’app Mac richiede {macRequirement}. Scegli Apple silicon o Intel nella pagina di download, oppure lascia scegliere la build a Homebrew."
+ "L’app Mac richiede {macRequirement} e funziona nativamente su Apple silicon e Intel. Scaricala dalla pagina di download oppure installala con Homebrew."
]
},
{
diff --git a/resources/data/content/ja/download.json b/resources/data/content/ja/download.json
index d90fd772..2127901d 100644
--- a/resources/data/content/ja/download.json
+++ b/resources/data/content/ja/download.json
@@ -33,7 +33,7 @@
"アプリケーションから TablePro を開きます。macOS の確認が表示されたら Open を選びます。",
"接続を追加するか、Help > Open Sample Database から同梱の Chinook SQLite データベースを試せます。"
],
- "signing": "どちらのビルドも Developer ID で署名され、Apple の公証を受けています。TablePro を開くためにセキュリティ設定を変更する必要はありません。",
+ "signing": "TablePro は Developer ID で署名され、Apple の公証を受けています。開くためにセキュリティ設定を変更する必要はありません。",
"drivers": "一般的なドライバーは同梱されています。その他は初回にインターネットからダウンロードし、アプリとは別に更新されます。",
"databases": "対応データベース",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "以前のバージョンとリリースノート",
- "body": "すべてのリリースのノートと両方のディスクイメージは GitHub Releases にあります。各バージョンの変更内容は変更履歴で確認できます。",
+ "body": "すべてのリリースのノートとディスクイメージは GitHub Releases にあります。各バージョンの変更内容は変更履歴で確認できます。",
"releases": "GitHub の全リリース",
"changelog": "変更履歴(英語)"
}
diff --git a/resources/data/content/ja/faq.json b/resources/data/content/ja/faq.json
index 411fb302..6a966639 100644
--- a/resources/data/content/ja/faq.json
+++ b/resources/data/content/ja/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "どの Mac で TablePro が動きますか?",
"answer": [
- "Mac アプリには {macRequirement} が必要です。ダウンロードページで Apple silicon・Intel を選ぶか、Homebrew に選択を任せられます。"
+ "Mac アプリには {macRequirement} が必要で、Apple silicon と Intel でネイティブ動作します。ダウンロードページから入手するか、Homebrew でインストールできます。"
]
},
{
diff --git a/resources/data/content/ko/download.json b/resources/data/content/ko/download.json
index 2819adb8..500089da 100644
--- a/resources/data/content/ko/download.json
+++ b/resources/data/content/ko/download.json
@@ -33,7 +33,7 @@
"응용 프로그램에서 TablePro를 여세요. macOS가 확인을 요청하면 열기 (Open)을 선택하세요.",
"연결을 추가하거나 도움말 > 샘플 데이터베이스 열기 (Help > Open Sample Database)에서 내장된 Chinook SQLite 데이터베이스를 사용해 보세요."
],
- "signing": "두 빌드 모두 Developer ID로 서명되었으며 Apple의 공증을 받았습니다. TablePro를 열기 위해 보안 설정을 변경할 필요가 없습니다.",
+ "signing": "TablePro는 Developer ID로 서명되었으며 Apple의 공증을 받았습니다. 열기 위해 보안 설정을 변경할 필요가 없습니다.",
"drivers": "일반적인 드라이버는 앱에 포함됩니다. 그 밖의 드라이버는 처음 사용할 때 인터넷으로 다운로드되며 앱과 별도로 업데이트됩니다.",
"databases": "지원 데이터베이스",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "이전 버전 및 릴리스 노트",
- "body": "모든 릴리스의 노트와 두 디스크 이미지는 GitHub Releases에 있습니다. 변경 기록에는 각 버전의 변경 사항이 나와 있습니다.",
+ "body": "모든 릴리스의 노트와 디스크 이미지는 GitHub Releases에 있습니다. 변경 기록에는 각 버전의 변경 사항이 나와 있습니다.",
"releases": "GitHub의 모든 릴리스",
"changelog": "변경 기록 (영어)"
}
diff --git a/resources/data/content/ko/faq.json b/resources/data/content/ko/faq.json
index f6c01a1e..577ff9b5 100644
--- a/resources/data/content/ko/faq.json
+++ b/resources/data/content/ko/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "어떤 Mac에서 TablePro를 사용할 수 있나요?",
"answer": [
- "Mac 앱에는 {macRequirement}이 필요합니다. 다운로드 페이지에서 Apple silicon 또는 Intel을 선택하거나 Homebrew로 빌드를 선택하세요."
+ "Mac 앱에는 {macRequirement}이 필요하며 Apple silicon과 Intel에서 네이티브로 실행됩니다. 다운로드 페이지에서 받거나 Homebrew로 설치하세요."
]
},
{
diff --git a/resources/data/content/pt-BR/download.json b/resources/data/content/pt-BR/download.json
index a208ad11..d865bf29 100644
--- a/resources/data/content/pt-BR/download.json
+++ b/resources/data/content/pt-BR/download.json
@@ -1,7 +1,7 @@
{
"seo": {
"title": "Baixar TablePro",
- "description": "Baixe TablePro, com compilações separadas para Apple silicon e Intel, ou instale-o com Homebrew. O aplicativo para iPhone e iPad está no App Store."
+ "description": "Baixe TablePro para Mac com Apple silicon e Intel, ou instale-o com Homebrew. O aplicativo para iPhone e iPad está no App Store."
},
"og": {
"kicker": "Download",
@@ -33,7 +33,7 @@
"Abra TablePro em Aplicativos. Escolha Open quando macOS solicitar sua confirmação.",
"Adicione uma conexão ou experimente o banco de dados Chinook SQLite incluído por meio de Help > Open Sample Database."
],
- "signing": "As duas versões são assinadas com Developer ID e autenticadas pela Apple, portanto você não precisa alterar configurações de segurança para abrir TablePro.",
+ "signing": "TablePro é assinado com Developer ID e autenticado pela Apple, portanto você não precisa alterar configurações de segurança para abri-lo.",
"drivers": "Drivers comuns estão incluídos. Outros precisam de uma conexão com a Internet para fazer o download no primeiro uso e atualizar separadamente do aplicativo.",
"databases": "Bancos de dados compatíveis",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "Versões anteriores e notas de versão",
- "body": "Todas as versões, com suas notas e as duas imagens de disco, estão no GitHub Releases. O histórico de alterações lista o que mudou em cada versão.",
+ "body": "Todas as versões, com suas notas e imagens de disco, estão no GitHub Releases. O histórico de alterações lista o que mudou em cada versão.",
"releases": "Todas as versões no GitHub",
"changelog": "Histórico de alterações (inglês)"
}
diff --git a/resources/data/content/pt-BR/faq.json b/resources/data/content/pt-BR/faq.json
index 139bf13d..0efe1664 100644
--- a/resources/data/content/pt-BR/faq.json
+++ b/resources/data/content/pt-BR/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "Em quais Macs TablePro funciona?",
"answer": [
- "O app para Mac exige {macRequirement}. Escolha Apple silicon ou Intel na página de download, ou deixe o Homebrew selecionar a versão."
+ "O app para Mac exige {macRequirement} e roda nativamente em Apple silicon e Intel. Baixe-o na página de download ou instale-o com o Homebrew."
]
},
{
diff --git a/resources/data/content/vi/download.json b/resources/data/content/vi/download.json
index 7d1c6182..9211ea85 100644
--- a/resources/data/content/vi/download.json
+++ b/resources/data/content/vi/download.json
@@ -1,7 +1,7 @@
{
"seo": {
"title": "Tải TablePro",
- "description": "Tải TablePro với bản build riêng cho Apple silicon và Intel, hoặc cài bằng Homebrew. Ứng dụng iPhone và iPad có trên App Store."
+ "description": "Tải TablePro cho máy Mac dùng Apple silicon và Intel, hoặc cài bằng Homebrew. Ứng dụng iPhone và iPad có trên App Store."
},
"og": {
"kicker": "Tải về",
@@ -33,7 +33,7 @@
"Mở TablePro từ Applications. Chọn Open khi macOS hỏi xác nhận.",
"Thêm connection, hoặc thử cơ sở dữ liệu SQLite Chinook có sẵn qua Help > Open Sample Database."
],
- "signing": "Cả hai bản build đều được ký bằng Developer ID và được Apple notarize, nên bạn không cần thay đổi cài đặt bảo mật nào để mở TablePro.",
+ "signing": "TablePro được ký bằng Developer ID và được Apple notarize, nên bạn không cần thay đổi cài đặt bảo mật nào để mở ứng dụng.",
"drivers": "Các driver phổ biến có sẵn. Driver khác cần internet để tải vào lần đầu sử dụng và cập nhật riêng.",
"databases": "Cơ sở dữ liệu được hỗ trợ",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "Phiên bản cũ và ghi chú phát hành",
- "body": "Mọi bản phát hành, kèm ghi chú và cả hai file DMG, đều có trên GitHub Releases. Changelog liệt kê những gì đã thay đổi trong từng phiên bản.",
+ "body": "Mọi bản phát hành, kèm ghi chú và file DMG, đều có trên GitHub Releases. Changelog liệt kê những gì đã thay đổi trong từng phiên bản.",
"releases": "Tất cả bản phát hành trên GitHub",
"changelog": "Changelog (tiếng Anh)"
}
diff --git a/resources/data/content/vi/faq.json b/resources/data/content/vi/faq.json
index e8d0e016..0eefc6a7 100644
--- a/resources/data/content/vi/faq.json
+++ b/resources/data/content/vi/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "TablePro chạy trên những máy Mac nào?",
"answer": [
- "Ứng dụng Mac cần {macRequirement}. Chọn Apple silicon hoặc Intel trên trang tải về, hoặc để Homebrew chọn bản build."
+ "Ứng dụng Mac cần {macRequirement} và chạy native trên Apple silicon và Intel. Tải về từ trang tải về hoặc cài bằng Homebrew."
]
},
{
diff --git a/resources/data/content/zh-Hans/download.json b/resources/data/content/zh-Hans/download.json
index a072d841..be4a1564 100644
--- a/resources/data/content/zh-Hans/download.json
+++ b/resources/data/content/zh-Hans/download.json
@@ -33,7 +33,7 @@
"从“应用程序”打开 TablePro。macOS 请求确认时,选择 打开 (Open)。",
"添加连接,或通过 帮助 > 打开示例数据库 (Help > Open Sample Database) 试用内置的 Chinook SQLite 示例数据库。"
],
- "signing": "两个构建版本均使用 Developer ID 签名并经 Apple 公证,打开 TablePro 无需更改任何安全设置。",
+ "signing": "TablePro 使用 Developer ID 签名并经 Apple 公证,打开时无需更改任何安全设置。",
"drivers": "常用驱动已内置。其他驱动在首次使用时需要联网下载,并独立于 App 更新。",
"databases": "受支持的数据库",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "旧版本和发行说明",
- "body": "每个版本的说明和两个磁盘映像均在 GitHub Releases 上。更新日志列出了各版本的改动。",
+ "body": "每个版本的说明和磁盘映像均在 GitHub Releases 上。更新日志列出了各版本的改动。",
"releases": "GitHub 上的所有版本",
"changelog": "更新日志(英语)"
}
diff --git a/resources/data/content/zh-Hans/faq.json b/resources/data/content/zh-Hans/faq.json
index 27fbdb58..19e80740 100644
--- a/resources/data/content/zh-Hans/faq.json
+++ b/resources/data/content/zh-Hans/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "哪些 Mac 可以运行 TablePro?",
"answer": [
- "Mac App 需要 {macRequirement}。在下载页面选择 Apple silicon 或 Intel,或由 Homebrew 自动选择版本。"
+ "Mac App 需要 {macRequirement},可在 Apple silicon 和 Intel 上原生运行。可从下载页面获取,或通过 Homebrew 安装。"
]
},
{
diff --git a/resources/data/content/zh-Hant/download.json b/resources/data/content/zh-Hant/download.json
index 997dae48..b11eb705 100644
--- a/resources/data/content/zh-Hant/download.json
+++ b/resources/data/content/zh-Hant/download.json
@@ -33,7 +33,7 @@
"從「應用程式」開啟 TablePro。macOS 要求確認時,選擇 開啟 (Open)。",
"新增連線,或透過 輔助說明 > 開啟範例資料庫 (Help > Open Sample Database) 試用內建的 Chinook SQLite 範例資料庫。"
],
- "signing": "兩個建置版本均使用 Developer ID 簽署並經 Apple 公證,開啟 TablePro 無需更改任何安全性設定。",
+ "signing": "TablePro 使用 Developer ID 簽署並經 Apple 公證,開啟時無需更改任何安全性設定。",
"drivers": "常用驅動程式已內建。其他驅動程式在首次使用時需要連網下載,並獨立於 App 更新。",
"databases": "支援的資料庫",
"guide": {
@@ -56,7 +56,7 @@
},
"olderVersions": {
"title": "舊版本與版本說明",
- "body": "每個版本的說明與兩個磁碟映像檔均在 GitHub Releases 上。更新紀錄列出了各版本的變更。",
+ "body": "每個版本的說明與磁碟映像檔均在 GitHub Releases 上。更新紀錄列出了各版本的變更。",
"releases": "GitHub 上的所有版本",
"changelog": "更新紀錄(英文)"
}
diff --git a/resources/data/content/zh-Hant/faq.json b/resources/data/content/zh-Hant/faq.json
index 9800f6a9..77f78286 100644
--- a/resources/data/content/zh-Hant/faq.json
+++ b/resources/data/content/zh-Hant/faq.json
@@ -74,7 +74,7 @@
"id": "mac-requirements",
"question": "哪些 Mac 可以執行 TablePro?",
"answer": [
- "Mac App 需要 {macRequirement}。在下載頁面選擇 Apple silicon 或 Intel,或由 Homebrew 自動選擇版本。"
+ "Mac App 需要 {macRequirement},可在 Apple silicon 和 Intel 上原生執行。可從下載頁面取得,或透過 Homebrew 安裝。"
]
},
{
diff --git a/resources/data/platforms.json b/resources/data/platforms.json
index f9593ccb..ad5b29c0 100644
--- a/resources/data/platforms.json
+++ b/resources/data/platforms.json
@@ -12,15 +12,13 @@
"displayVersion": "13",
"releaseName": "Ventura"
},
- "architectures": [
- { "id": "arm64", "assetTemplate": "TablePro-{version}-arm64.dmg" },
- { "id": "x86_64", "assetTemplate": "TablePro-{version}-x86_64.dmg" }
- ],
- "universalBinary": false,
+ "architectures": ["arm64", "x86_64"],
+ "universalBinary": true,
+ "assetTemplate": "TablePro-{version}.dmg",
"destinations": [
{
"kind": "dmg",
- "urlTemplate": "https://github.com/TableProApp/TablePro/releases/download/v{version}/TablePro-{version}-{arch}.dmg"
+ "urlTemplate": "https://github.com/TableProApp/TablePro/releases/download/v{version}/TablePro-{version}.dmg"
},
{
"kind": "homebrew",
diff --git a/resources/js/components/compare/model.ts b/resources/js/components/compare/model.ts
index f2b106a6..37fcea4d 100644
--- a/resources/js/components/compare/model.ts
+++ b/resources/js/components/compare/model.ts
@@ -65,6 +65,8 @@ export interface TableProFacts {
/** `iOS and iPadOS 18 or later`, or null while there is no iPhone and iPad app. */
iosRequirement: string | null;
macArchitectures: ('arm64' | 'x86_64')[];
+ /** One app for both architectures, from `platforms.json`'s `universalBinary`. */
+ macUniversal: boolean;
/** `activations`: the Macs one Starter license covers. */
starter: { monthly: number; yearly: number; lifetime: number; activations: number };
team: { monthly: number; yearly: number; lifetime: number; minSeats: number };
@@ -508,7 +510,7 @@ export function tableproCell(row: Exclude | 'technology',
switch (row) {
case 'platforms': {
const lines: CellLine[] = [{ text: facts.devices }, { text: facts.macRequirement, muted: true }];
- const builds = buildsLabel(facts.macArchitectures, labels);
+ const builds = buildsLabel(facts.macArchitectures, labels, facts.macUniversal);
if (builds !== null) {
lines.push({ text: builds, muted: true });
diff --git a/resources/js/components/compare/tablepro-facts.ts b/resources/js/components/compare/tablepro-facts.ts
index 23c7ff8a..866afd2b 100644
--- a/resources/js/components/compare/tablepro-facts.ts
+++ b/resources/js/components/compare/tablepro-facts.ts
@@ -27,7 +27,8 @@ export function tableproFacts(productId: string | null, featuredEngines: string[
devices: deviceList(m.common.list),
macRequirement: requirementText(mac, m.platforms),
iosRequirement: ios !== null ? requirementText(ios, m.platforms) : null,
- macArchitectures: mac.architectures.map((architecture) => architecture.id),
+ macArchitectures: mac.architectures,
+ macUniversal: mac.universalBinary,
starter: { ...PRICING.tiers.starter.prices, activations: PRICING.tiers.starter.activations },
team: { ...PRICING.tiers.team.prices, minSeats: PRICING.tiers.team.seats.min },
iosFree: ios !== null && ios.price.amount === 0 && !ios.price.inAppPurchases,
diff --git a/resources/js/components/download/action-pair.tsx b/resources/js/components/download/action-pair.tsx
index 4000ccf8..7f3a78aa 100644
--- a/resources/js/components/download/action-pair.tsx
+++ b/resources/js/components/download/action-pair.tsx
@@ -34,7 +34,7 @@ interface ActionPairProps {
* had drifted apart in button size, glyph and caption spacing.
*
* "Download for Mac" is the large primary button with the Apple glyph and
- * goes to /download, which offers both builds; it never starts a download.
+ * goes to /download, which offers the DMG; it never starts a download.
* The App Store badge (40px, Apple's minimum) sits centred in a 48px row, the
* button's height, so the two captions start on one line. Side by side from
* 640px, stacked below, each caption under its own action.
diff --git a/resources/js/components/download/mac-card.tsx b/resources/js/components/download/mac-card.tsx
index 4c867b00..f21cf379 100644
--- a/resources/js/components/download/mac-card.tsx
+++ b/resources/js/components/download/mac-card.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState, type ReactNode } from 'react';
+import { useState, type ReactNode } from 'react';
import { ChevronRight, Download } from 'lucide-react';
import Badge from '@/components/ui/badge';
import Button from '@/components/ui/button';
@@ -7,13 +7,11 @@ import { InlineCode } from '@/components/ui/code';
import TextLink, { textLinkClasses } from '@/components/ui/text-link';
import { Trans, useI18n } from '@/i18n';
import { trackDownload } from '@/lib/analytics';
-import { buildVariant, chipHelpOpen, type DeviceKind, type MacArch } from '@/lib/device';
+import type { DeviceKind } from '@/lib/device';
import { cn } from '@/lib/utils';
import CommandBlock from './command-block';
import { megabytes, requirementLine } from './format';
-import type { DownloadContent, LinksProp, MacProp, ReleaseAsset, ReleaseProp } from './types';
-
-const BUILDS: readonly MacArch[] = ['arm64', 'x86_64'];
+import type { DownloadContent, LinksProp, MacProp, ReleaseProp } from './types';
/** `…` in the copy marks an app label or menu path. */
export const UI_TAGS = { ui: (text: string) => {text} };
@@ -25,42 +23,27 @@ interface MacCardProps {
links: LinksProp;
/** Null until mounted: the server renders the same markup for every device. */
device: DeviceKind | null;
- /** Chromium's architecture hint, null when the browser gave none (Safari, Firefox), undefined until it has answered. */
- hint: MacArch | null | undefined;
className?: string;
}
/**
- * The Mac card (`#mac`): the release badge, both builds, the help for choosing
- * one, each build's SHA-256 when the release data carries it, and the other
- * ways to install (design-system §5.3.18 `PlatformCard`,
- * §8.7). It is a cell of the download page's `CellGrid`, so it draws no box of
- * its own, and its inner rule runs from wall to wall (§4.7).
+ * The Mac card (`#mac`): the release badge, the universal DMG, its SHA-256
+ * when the release data carries it, and the other ways to install
+ * (design-system §5.3.18 `PlatformCard`, §8.7). It is a cell of the download
+ * page's `CellGrid`, so it draws no box of its own, and its inner rule runs
+ * from wall to wall (§4.7).
*
- * Both DMG links are rendered on the server with their real URLs. The
- * browser's hint can only make one of them primary; with no hint both stay
- * equal and "Which Mac do I have?" opens. Nothing navigates on its own, and
- * nothing claims a download started:
- * the "Next, install it" panel appears only after the reader clicked a build,
- * and offers the same file again in case nothing arrived.
+ * The DMG link is rendered on the server with its real URL. Nothing navigates
+ * on its own, and nothing claims a download started: the "Next, install it"
+ * panel appears only after the reader clicked the button, and offers the same
+ * file again in case nothing arrived.
*/
-export default function MacCard({ content, release, mac, links, device, hint, className }: MacCardProps) {
+export default function MacCard({ content, release, mac, links, device, className }: MacCardProps) {
const { m, fmt } = useI18n();
- const [clicked, setClicked] = useState(null);
- const help = useRef(null);
- const openHelp = chipHelpOpen(device, hint);
-
- useEffect(() => {
- if (openHelp && help.current) {
- help.current.open = true;
- }
- }, [openHelp]);
+ const [clicked, setClicked] = useState(false);
const available = release.source !== 'unavailable';
- const chosen = clicked !== null ? release.assets[clicked] : null;
- const checksums = BUILDS.map((build) => release.assets[build]).filter(
- (asset): asset is ReleaseAsset & { name: string; sha256: string } => asset.name !== null && asset.sha256 !== null,
- );
+ const { asset } = release;
let badge: string | null = null;
@@ -74,9 +57,9 @@ export default function MacCard({ content, release, mac, links, device, hint, cl
/**
* The file name in Plex Mono, as identifiers are (design-system §3.1), and
* only the size in tabular figures: tabular figures in Inter spaced the
- * name's hyphens out ("TablePro - 0.77.0 - arm64.dmg").
+ * name's hyphen out ("TablePro - 0.82.0.dmg").
*/
- function fileLabel(asset: ReleaseAsset): ReactNode {
+ function fileLabel(): ReactNode {
if (asset.name === null) {
return null;
}
@@ -95,13 +78,8 @@ export default function MacCard({ content, release, mac, links, device, hint, cl
);
}
- let status = '';
-
- if (device === 'ios' || device === 'other') {
- status = m.download.onAnotherDevice;
- } else if (device === 'mac' && hint) {
- status = fmt(m.download.detected, { chip: m.platforms.architectures[hint] });
- }
+ const file = fileLabel();
+ const status = device === 'ios' || device === 'other' ? m.download.onAnotherDevice : '';
return (
@@ -117,39 +95,32 @@ export default function MacCard({ content, release, mac, links, device, hint, cl
{/*
- * Room reserved on the server, so the detected chip appears
- * without moving anything below it: two lines below 640px, where
- * either sentence wraps in a phone-width card, one above. Not a
+ * Room reserved on the server, so the sentence for another device
+ * appears without moving anything below it: two lines below
+ * 640px, where it wraps in a phone-width card, one above. Not a
* live region: it changes on load, not in answer to anything the
* reader did.
*/}
@@ -157,18 +128,7 @@ export default function MacCard({ content, release, mac, links, device, hint, cl
{status}
-
-
-
- {m.download.whichMac.summary}
-
-
(
- trackDownload('download-retry', 'mac')} className={textLinkClasses('inline')}>
+ trackDownload('download-retry', 'mac')} className={textLinkClasses('inline')}>
{text}
),
diff --git a/resources/js/components/download/types.ts b/resources/js/components/download/types.ts
index 6fb9a01c..635ac853 100644
--- a/resources/js/components/download/types.ts
+++ b/resources/js/components/download/types.ts
@@ -4,7 +4,6 @@
* holds every other locale to the same keys.
*/
import type { PostSummary } from '@/components/blog/post-list';
-import type { MacArch } from '@/lib/device';
import type { Requirements } from '@/lib/data/platforms';
export type { Requirements };
@@ -12,7 +11,7 @@ export type { Requirements };
export type DownloadContent = typeof import('@data/content/en/download.json');
export interface ReleaseAsset {
- /** The DMG's file name, or null when the release is unavailable. */
+ /** The universal DMG's file name, or null when the release is unavailable. */
name: string | null;
/** The DMG, or GitHub's latest-release page when the release is unavailable. */
url: string;
@@ -27,7 +26,7 @@ export interface ReleaseProp {
publishedAt: string | null;
/** Formatted in PHP for the page's locale, so SSR and the browser agree. */
publishedAtFormatted: string | null;
- assets: Record;
+ asset: ReleaseAsset;
releaseUrl: string;
releasesUrl: string;
source: 'github' | 'appcast' | 'unavailable';
diff --git a/resources/js/components/home/availability.ts b/resources/js/components/home/availability.ts
index 4c63e108..a316b80d 100644
--- a/resources/js/components/home/availability.ts
+++ b/resources/js/components/home/availability.ts
@@ -81,11 +81,11 @@ export function availability(m: Messages, captions: { mac: string; ios: string }
};
}
-/** The Mac build architectures as a plain list: "Apple silicon and Intel". */
+/** The Mac architectures as a plain list: "Apple silicon and Intel". */
export function macArchitectureList(m: Messages): string {
return keepTogether(
joinList(
- macPlatform().architectures.map((architecture) => m.platforms.architectures[architecture.id]),
+ macPlatform().architectures.map((architecture) => m.platforms.architectures[architecture]),
m.common.list,
),
);
diff --git a/resources/js/i18n/messages/de/download.ts b/resources/js/i18n/messages/de/download.ts
index 83bc0ef0..0a91db62 100644
--- a/resources/js/i18n/messages/de/download.ts
+++ b/resources/js/i18n/messages/de/download.ts
@@ -2,17 +2,13 @@ import type { Messages } from '../../types.ts';
export default {
"macCta": "Für Mac herunterladen",
- "builds": {
- "arm64": "Für Apple silicon herunterladen",
- "x86_64": "Für Intel herunterladen"
- },
"release": {
"dated": "Version {version}, veröffentlicht am {date}",
"undated": "Version {version}",
"badge": "v{version} · {date}",
"badgeUndated": "v{version}",
"notes": "Versionshinweise (Englisch)",
- "unavailable": "Die Versionsdetails sind nicht verfügbar. Beide Schaltflächen öffnen die neueste Version auf GitHub; wähle dort einen Build."
+ "unavailable": "Die Versionsdetails sind nicht verfügbar. Die Schaltfläche öffnet die neueste Version auf GitHub."
},
"file": {
"sized": "{name} · {size} MB",
@@ -22,12 +18,7 @@ export default {
"group": "."
}
},
- "detected": "Dein Browser meldet einen Mac mit {chip}.",
"onAnotherDevice": "Öffne diese Seite auf deinem Mac, um die Mac-App zu installieren.",
- "whichMac": {
- "summary": "Welchen Mac habe ich?",
- "body": "Wähle im Apple-Menü „Über diesen Mac“. Die Zeile „Chip“ bedeutet Apple silicon; eine Zeile „Prozessor“ mit Intel bedeutet Intel."
- },
"checksum": {
"summary": "Download überprüfen",
"body": "Führe im Terminal shasum -a 256 für die Datei aus. Das Ergebnis muss mit der SHA-256-Prüfsumme unten übereinstimmen."
diff --git a/resources/js/i18n/messages/en/download.ts b/resources/js/i18n/messages/en/download.ts
index e6868f00..02f208f5 100644
--- a/resources/js/i18n/messages/en/download.ts
+++ b/resources/js/i18n/messages/en/download.ts
@@ -6,32 +6,25 @@
* purchases", "Linux") are the `platforms` namespace's.
*
* - `macCta` is the availability-layer Mac action (positioning §4), for the
- * hero, the closing band, the mobile menu and the Free plan.
- * - `builds` are the two architecture buttons; the browser may highlight one,
- * never choose (resources/js/lib/device.ts).
+ * hero, the closing band, the mobile menu, the Free plan and the DMG button.
* - `release.badge` is the version badge on the Mac card; `{date}` arrives
* formatted by the server for the page's locale.
* - `otherPlatforms.joiner` joins the platform names in a negative sentence,
* so the last one takes "or": "not available for Linux or Windows".
- * - `checksum` is the disclosure under the builds. It renders only when the
- * release data carries a SHA-256 for a build.
+ * - `checksum` is the disclosure under the DMG button. It renders only when
+ * the release data carries the DMG's SHA-256.
* - Nothing here says a download has started. `afterClick` appears only after
- * the reader clicked a build, and says what to do if nothing arrived.
+ * the reader clicked the DMG button, and says what to do if nothing arrived.
*/
export default {
macCta: 'Download for Mac',
- builds: {
- arm64: 'Download for Apple silicon',
- x86_64: 'Download for Intel',
- },
release: {
dated: 'Version {version}, released {date}',
undated: 'Version {version}',
badge: 'v{version} · {date}',
badgeUndated: 'v{version}',
notes: 'Release notes',
- unavailable:
- 'Release details are unavailable. Both buttons open the latest release on GitHub; choose a build there.',
+ unavailable: 'Release details are unavailable. The button opens the latest release on GitHub.',
},
file: {
sized: '{name} · {size} MB',
@@ -41,12 +34,7 @@ export default {
group: ',',
},
},
- detected: 'Your browser reports a Mac with {chip}.',
onAnotherDevice: 'To install the Mac app, open this page on your Mac.',
- whichMac: {
- summary: 'Which Mac do I have?',
- body: 'Choose About This Mac in the Apple menu. A Chip line means Apple silicon; a Processor line naming Intel means Intel.',
- },
checksum: {
summary: 'Verify your download',
body: 'Run shasum -a 256 on the file in Terminal. The result should match the SHA-256 checksum below.',
diff --git a/resources/js/i18n/messages/es/download.ts b/resources/js/i18n/messages/es/download.ts
index ca45415c..324f3a2e 100644
--- a/resources/js/i18n/messages/es/download.ts
+++ b/resources/js/i18n/messages/es/download.ts
@@ -2,17 +2,13 @@ import type { Messages } from '../../types.ts';
export default {
"macCta": "Descargar para Mac",
- "builds": {
- "arm64": "Descargar para Apple silicon",
- "x86_64": "Descargar para Intel"
- },
"release": {
"dated": "Versión {version}, publicada el {date}",
"undated": "Versión {version}",
"badge": "v{version} · {date}",
"badgeUndated": "v{version}",
"notes": "Notas de versión (inglés)",
- "unavailable": "Los detalles de la versión no están disponibles. Ambos botones abren la última versión en GitHub; elige allí una compilación."
+ "unavailable": "Los detalles de la versión no están disponibles. El botón abre la última versión en GitHub."
},
"file": {
"sized": "{name} · {size} MB",
@@ -22,12 +18,7 @@ export default {
"group": "."
}
},
- "detected": "Tu navegador indica que tienes un Mac con {chip}.",
"onAnotherDevice": "Para instalar la app para Mac, abre esta página en tu Mac.",
- "whichMac": {
- "summary": "¿Qué Mac tengo?",
- "body": "Elige «Acerca de este Mac» en el menú Apple. Una línea «Chip» indica Apple silicon; una línea «Procesador» que menciona Intel indica Intel."
- },
"checksum": {
"summary": "Verifica tu descarga",
"body": "Ejecuta shasum -a 256 sobre el archivo en Terminal. El resultado debe coincidir con la suma SHA-256 de abajo."
diff --git a/resources/js/i18n/messages/fr/download.ts b/resources/js/i18n/messages/fr/download.ts
index 699f15ea..87366df4 100644
--- a/resources/js/i18n/messages/fr/download.ts
+++ b/resources/js/i18n/messages/fr/download.ts
@@ -2,17 +2,13 @@ import type { Messages } from '../../types.ts';
export default {
"macCta": "Télécharger pour Mac",
- "builds": {
- "arm64": "Télécharger pour Apple silicon",
- "x86_64": "Télécharger pour Intel"
- },
"release": {
"dated": "Version {version}, publiée le {date}",
"undated": "Version {version}",
"badge": "v{version} · {date}",
"badgeUndated": "v{version}",
"notes": "Notes de version (anglais)",
- "unavailable": "Les détails de version sont indisponibles. Les deux boutons ouvrent la dernière version sur GitHub ; choisissez-y une version de l’app."
+ "unavailable": "Les détails de version sont indisponibles. Le bouton ouvre la dernière version sur GitHub."
},
"file": {
"sized": "{name} · {size} Mo",
@@ -22,12 +18,7 @@ export default {
"group": " "
}
},
- "detected": "Votre navigateur indique un Mac avec {chip}.",
"onAnotherDevice": "Pour installer l’application Mac, ouvrez cette page sur votre Mac.",
- "whichMac": {
- "summary": "Quel Mac ai-je ?",
- "body": "Choisissez « À propos de ce Mac » dans le menu Apple. La ligne « Puce » indique Apple silicon ; la ligne « Processeur » mentionnant Intel indique Intel."
- },
"checksum": {
"summary": "Vérifier votre téléchargement",
"body": "Exécutez shasum -a 256 sur le fichier dans Terminal. Le résultat doit correspondre à la somme de contrôle SHA-256 ci-dessous."
diff --git a/resources/js/i18n/messages/id/download.ts b/resources/js/i18n/messages/id/download.ts
index ad85e48e..3d97649e 100644
--- a/resources/js/i18n/messages/id/download.ts
+++ b/resources/js/i18n/messages/id/download.ts
@@ -2,17 +2,13 @@ import type { Messages } from '../../types.ts';
export default {
"macCta": "Unduh untuk Mac",
- "builds": {
- "arm64": "Unduh untuk Apple silicon",
- "x86_64": "Unduh untuk Intel"
- },
"release": {
"dated": "Versi {version}, dirilis {date}",
"undated": "Versi {version}",
"badge": "v{version} · {date}",
"badgeUndated": "v{version}",
"notes": "Catatan rilis (bahasa Inggris)",
- "unavailable": "Detail rilis tidak tersedia. Kedua tombol membuka rilis terbaru di GitHub; pilih build di sana."
+ "unavailable": "Detail rilis tidak tersedia. Tombol ini membuka rilis terbaru di GitHub."
},
"file": {
"sized": "{name} · {size} MB",
@@ -22,12 +18,7 @@ export default {
"group": "."
}
},
- "detected": "Browser Anda melaporkan Mac dengan {chip}.",
"onAnotherDevice": "Untuk menginstal aplikasi Mac, buka halaman ini di Mac Anda.",
- "whichMac": {
- "summary": "Mac apa yang saya miliki?",
- "body": "Pilih Mengenai Mac Ini di menu Apple. Baris Chip menunjukkan Apple silicon; baris Prosesor yang menyebut Intel menunjukkan Intel."
- },
"checksum": {
"summary": "Verifikasi unduhan Anda",
"body": "Jalankan shasum -a 256 pada file tersebut di Terminal. Hasilnya harus sama dengan checksum SHA-256 di bawah."
diff --git a/resources/js/i18n/messages/it/download.ts b/resources/js/i18n/messages/it/download.ts
index 84afd2bb..0a362d1f 100644
--- a/resources/js/i18n/messages/it/download.ts
+++ b/resources/js/i18n/messages/it/download.ts
@@ -2,17 +2,13 @@ import type { Messages } from '../../types.ts';
export default {
"macCta": "Scarica per Mac",
- "builds": {
- "arm64": "Scarica per Apple silicon",
- "x86_64": "Scarica per Intel"
- },
"release": {
"dated": "Versione {version}, rilasciata il {date}",
"undated": "Versione {version}",
"badge": "v{version} · {date}",
"badgeUndated": "v{version}",
"notes": "Note di rilascio (inglese)",
- "unavailable": "Dettagli del rilascio non disponibili. Entrambi i pulsanti aprono l’ultima versione su GitHub; scegli lì la build."
+ "unavailable": "Dettagli del rilascio non disponibili. Il pulsante apre l’ultima versione su GitHub."
},
"file": {
"sized": "{name} · {size} MB",
@@ -22,12 +18,7 @@ export default {
"group": "."
}
},
- "detected": "Il browser indica un Mac con {chip}.",
"onAnotherDevice": "Per installare l’app per Mac, apri questa pagina sul tuo Mac.",
- "whichMac": {
- "summary": "Quale Mac ho?",
- "body": "Scegli Informazioni su questo Mac dal menu Apple. La voce Chip indica Apple silicon; una voce Processore con Intel indica Intel."
- },
"checksum": {
"summary": "Verifica il download",
"body": "Esegui shasum -a 256 sul file nel Terminale. Il risultato deve corrispondere al checksum SHA-256 qui sotto."
diff --git a/resources/js/i18n/messages/ja/download.ts b/resources/js/i18n/messages/ja/download.ts
index 13800dda..3b9202fd 100644
--- a/resources/js/i18n/messages/ja/download.ts
+++ b/resources/js/i18n/messages/ja/download.ts
@@ -2,17 +2,13 @@ import type { Messages } from '../../types.ts';
export default {
"macCta": "Mac 版をダウンロード",
- "builds": {
- "arm64": "Apple silicon 版をダウンロード",
- "x86_64": "Intel 版をダウンロード"
- },
"release": {
"dated": "バージョン {version}、{date} リリース",
"undated": "バージョン {version}",
"badge": "v{version} · {date}",
"badgeUndated": "v{version}",
"notes": "リリースノート(英語)",
- "unavailable": "リリース情報を取得できません。どちらのボタンも GitHub の最新リリースを開きます。そこでビルドを選んでください。"
+ "unavailable": "リリース情報を取得できません。ボタンは GitHub の最新リリースを開きます。"
},
"file": {
"sized": "{name} · {size} MB",
@@ -22,12 +18,7 @@ export default {
"group": ","
}
},
- "detected": "ブラウザーによると、お使いの Mac は {chip} 搭載です。",
"onAnotherDevice": "Mac アプリをインストールするには、Mac でこのページを開いてください。",
- "whichMac": {
- "summary": "自分の Mac の種類を確認する",
- "body": "Apple メニューで「この Mac について」を選びます。「チップ」欄があれば Apple silicon Mac、「プロセッサ」欄に Intel とあれば Intel Mac です。"
- },
"checksum": {
"summary": "ダウンロードを検証する",
"body": "ターミナルでファイルに対して shasum -a 256 を実行します。結果が下の SHA-256 チェックサムと一致することを確認してください。"
diff --git a/resources/js/i18n/messages/ko/download.ts b/resources/js/i18n/messages/ko/download.ts
index d0f41e69..9b3380b7 100644
--- a/resources/js/i18n/messages/ko/download.ts
+++ b/resources/js/i18n/messages/ko/download.ts
@@ -2,17 +2,13 @@ import type { Messages } from '../../types.ts';
export default {
"macCta": "Mac용 다운로드",
- "builds": {
- "arm64": "Apple silicon용 다운로드",
- "x86_64": "Intel용 다운로드"
- },
"release": {
"dated": "버전 {version}, {date} 출시",
"undated": "버전 {version}",
"badge": "v{version} · {date}",
"badgeUndated": "v{version}",
"notes": "릴리스 노트 (영어)",
- "unavailable": "릴리스 정보를 불러올 수 없습니다. 두 버튼 모두 GitHub의 최신 릴리스를 엽니다. 거기에서 빌드를 선택하세요."
+ "unavailable": "릴리스 정보를 불러올 수 없습니다. 버튼을 누르면 GitHub의 최신 릴리스가 열립니다."
},
"file": {
"sized": "{name} · {size} MB",
@@ -22,12 +18,7 @@ export default {
"group": ","
}
},
- "detected": "브라우저 정보에 따르면 {chip} 탑재 Mac을 사용 중입니다.",
"onAnotherDevice": "Mac 앱을 설치하려면 Mac에서 이 페이지를 여세요.",
- "whichMac": {
- "summary": "내 Mac은 어떤 모델인가요?",
- "body": "Apple 메뉴에서 '이 Mac에 관하여'를 선택하세요. 칩 항목이 있으면 Apple silicon Mac이며, 프로세서 항목에 Intel이 표시되면 Intel Mac입니다."
- },
"checksum": {
"summary": "다운로드 파일 확인",
"body": "터미널에서 파일에 shasum -a 256을 실행하세요. 결과가 아래 SHA-256 체크섬과 일치해야 합니다."
diff --git a/resources/js/i18n/messages/pt-BR/download.ts b/resources/js/i18n/messages/pt-BR/download.ts
index 27301dd0..46dac043 100644
--- a/resources/js/i18n/messages/pt-BR/download.ts
+++ b/resources/js/i18n/messages/pt-BR/download.ts
@@ -2,17 +2,13 @@ import type { Messages } from '../../types.ts';
export default {
"macCta": "Baixar para Mac",
- "builds": {
- "arm64": "Baixar para Apple silicon",
- "x86_64": "Baixar para Intel"
- },
"release": {
"dated": "Versão {version}, lançada em {date}",
"undated": "Versão {version}",
"badge": "v{version} · {date}",
"badgeUndated": "v{version}",
"notes": "Notas de versão (inglês)",
- "unavailable": "Detalhes do lançamento indisponíveis. Os dois botões abrem a última versão no GitHub; escolha a versão lá."
+ "unavailable": "Detalhes do lançamento indisponíveis. O botão abre a última versão no GitHub."
},
"file": {
"sized": "{name} · {size} MB",
@@ -22,12 +18,7 @@ export default {
"group": "."
}
},
- "detected": "Seu navegador informa que seu Mac usa {chip}.",
"onAnotherDevice": "Para instalar o app para Mac, abra esta página no seu Mac.",
- "whichMac": {
- "summary": "Qual é o meu Mac?",
- "body": "Escolha Sobre Este Mac no menu Apple. A linha Chip indica Apple silicon; a linha Processador com Intel indica Intel."
- },
"checksum": {
"summary": "Verifique seu download",
"body": "Execute shasum -a 256 no arquivo pelo Terminal. O resultado deve ser igual à soma de verificação SHA-256 abaixo."
diff --git a/resources/js/i18n/messages/vi/download.ts b/resources/js/i18n/messages/vi/download.ts
index 2dd0c652..1ec5ad65 100644
--- a/resources/js/i18n/messages/vi/download.ts
+++ b/resources/js/i18n/messages/vi/download.ts
@@ -7,18 +7,13 @@ import type { Messages } from '../../types.ts';
*/
export default {
macCta: 'Tải về cho Mac',
- builds: {
- arm64: 'Tải bản cho Apple silicon',
- x86_64: 'Tải bản cho Intel',
- },
release: {
dated: 'Phiên bản {version}, phát hành ngày {date}',
undated: 'Phiên bản {version}',
badge: 'v{version} · {date}',
badgeUndated: 'v{version}',
notes: 'Ghi chú phát hành (tiếng Anh)',
- unavailable:
- 'Không tải được thông tin bản phát hành. Cả hai nút mở bản mới nhất trên GitHub; chọn bản build tại đó.',
+ unavailable: 'Không tải được thông tin bản phát hành. Nút tải về mở bản mới nhất trên GitHub.',
},
file: {
sized: '{name} · {size} MB',
@@ -28,12 +23,7 @@ export default {
group: '.',
},
},
- detected: 'Trình duyệt cho biết máy Mac của bạn dùng {chip}.',
onAnotherDevice: 'Để cài ứng dụng Mac, hãy mở trang này trên máy Mac của bạn.',
- whichMac: {
- summary: 'Máy Mac của bạn dùng chip nào?',
- body: 'Chọn Giới thiệu về máy Mac này (About This Mac) trong menu Apple. Mục Chip nghĩa là Apple silicon; mục Bộ xử lý (Processor) ghi Intel nghĩa là Intel.',
- },
checksum: {
summary: 'Kiểm tra file đã tải về',
body: 'Chạy shasum -a 256 với file đó trong Terminal. Kết quả phải khớp với checksum SHA-256 bên dưới.',
diff --git a/resources/js/i18n/messages/zh-Hans/download.ts b/resources/js/i18n/messages/zh-Hans/download.ts
index 90b37400..037ab168 100644
--- a/resources/js/i18n/messages/zh-Hans/download.ts
+++ b/resources/js/i18n/messages/zh-Hans/download.ts
@@ -2,17 +2,13 @@ import type { Messages } from '../../types.ts';
export default {
"macCta": "下载 Mac 版",
- "builds": {
- "arm64": "下载 Apple silicon 版",
- "x86_64": "下载 Intel 版"
- },
"release": {
"dated": "版本 {version},发布于 {date}",
"undated": "版本 {version}",
"badge": "v{version} · {date}",
"badgeUndated": "v{version}",
"notes": "发行说明(英语)",
- "unavailable": "无法获取版本信息。两个按钮都会打开 GitHub 上的最新版本,请在那里选择安装包。"
+ "unavailable": "无法获取版本信息。按钮会打开 GitHub 上的最新版本。"
},
"file": {
"sized": "{name} · {size} MB",
@@ -22,12 +18,7 @@ export default {
"group": ","
}
},
- "detected": "浏览器显示您的 Mac 使用 {chip}。",
"onAnotherDevice": "要安装 Mac 应用,请在 Mac 上打开此页面。",
- "whichMac": {
- "summary": "我的 Mac 是哪种型号?",
- "body": "在 Apple 菜单中选择“关于本机”。显示“芯片”一栏的是 Apple 芯片 Mac;“处理器”一栏显示 Intel 的是 Intel Mac。"
- },
"checksum": {
"summary": "验证下载的文件",
"body": "在终端中对该文件运行 shasum -a 256,结果应与下方的 SHA-256 校验和一致。"
diff --git a/resources/js/i18n/messages/zh-Hant/download.ts b/resources/js/i18n/messages/zh-Hant/download.ts
index b1888ab7..28d5ff82 100644
--- a/resources/js/i18n/messages/zh-Hant/download.ts
+++ b/resources/js/i18n/messages/zh-Hant/download.ts
@@ -2,17 +2,13 @@ import type { Messages } from '../../types.ts';
export default {
"macCta": "下載 Mac 版",
- "builds": {
- "arm64": "下載 Apple silicon 版",
- "x86_64": "下載 Intel 版"
- },
"release": {
"dated": "版本 {version},發佈於 {date}",
"undated": "版本 {version}",
"badge": "v{version} · {date}",
"badgeUndated": "v{version}",
"notes": "版本說明(英文)",
- "unavailable": "無法取得版本資訊。兩個按鈕都會開啟 GitHub 上的最新版本,請在那裡選擇安裝套件。"
+ "unavailable": "無法取得版本資訊。按鈕會開啟 GitHub 上的最新版本。"
},
"file": {
"sized": "{name} · {size} MB",
@@ -22,12 +18,7 @@ export default {
"group": ","
}
},
- "detected": "瀏覽器顯示您的 Mac 使用 {chip}。",
"onAnotherDevice": "若要安裝 Mac App,請在 Mac 上開啟此頁面。",
- "whichMac": {
- "summary": "我的 Mac 是哪種型號?",
- "body": "在 Apple 選單中選擇「關於這台 Mac」。顯示「晶片」欄位的是 Apple 晶片 Mac;「處理器」欄位顯示 Intel 的是 Intel Mac。"
- },
"checksum": {
"summary": "驗證下載的檔案",
"body": "在終端機中對該檔案執行 shasum -a 256,結果應與下方的 SHA-256 檢查碼一致。"
diff --git a/resources/js/lib/data/platforms.ts b/resources/js/lib/data/platforms.ts
index 10ccb3cd..91626bfc 100644
--- a/resources/js/lib/data/platforms.ts
+++ b/resources/js/lib/data/platforms.ts
@@ -33,12 +33,6 @@ export interface Requirements {
releaseName: string | null;
}
-export interface Architecture {
- id: MacArchitecture;
- /** The DMG file name, with `{version}` for the release version. */
- assetTemplate: string;
-}
-
export type Destination =
| { kind: 'dmg'; urlTemplate: string }
| { kind: 'homebrew'; command: string; url: string }
@@ -70,9 +64,12 @@ interface ReleasedPlatformBase {
export interface MacPlatform extends ReleasedPlatformBase {
id: 'mac';
- architectures: Architecture[];
- /** Two separate builds. Never "Universal". */
- universalBinary: false;
+ /** The chips the app runs on natively, in display order. */
+ architectures: MacArchitecture[];
+ /** One DMG runs on every architecture. */
+ universalBinary: boolean;
+ /** The DMG file name, with `{version}` for the release version. */
+ assetTemplate: string;
/** The oldest version any channel still serves. */
floorVersion: string;
}
@@ -167,10 +164,10 @@ export function requirementText(platform: ReleasedPlatform, strings: PlatformStr
return interpolate(releaseName === null ? strings.requirement.unnamed : strings.requirement.named, values);
}
-/** `Apple silicon or Intel`, in the Mac build order. */
+/** `Apple silicon or Intel`, in the data order. */
export function architecturesText(strings: PlatformStrings): string {
return joinList(
- macPlatform().architectures.map((architecture) => strings.architectures[architecture.id]),
+ macPlatform().architectures.map((architecture) => strings.architectures[architecture]),
{ separator: ', ', last: strings.architectures.joiner },
);
}
diff --git a/resources/js/lib/device.ts b/resources/js/lib/device.ts
index 8064ef05..15b2d16b 100644
--- a/resources/js/lib/device.ts
+++ b/resources/js/lib/device.ts
@@ -1,19 +1,13 @@
/**
- * What the download page may infer about the reader's device, and what it may
- * not (architecture §1.13).
+ * What the download page may infer about the reader's device (architecture
+ * §1.13). The page never acts on it: the Mac DMG is a server-rendered link,
+ * and nothing starts a download on its own. The result only decides whether
+ * the App Store card comes first, and whether the Mac card says to open the
+ * page on a Mac.
*
- * The page never acts on any of this. Both Mac builds are server-rendered
- * links, and nothing starts a download on its own. The result only decides
- * emphasis: which build's button is primary, and whether the App Store card
- * comes first.
- *
- * - An iPhone or iPad is never a Mac. iPhone user agents contain "like Mac OS
- * X", which is how the pre-rebuild page sent iPhones to the Apple silicon
- * DMG. iPadOS Safari sends a desktop "Macintosh" user agent by default, so a
- * "Macintosh" with more than one touch point is an iPad.
- * - Only Chromium's `architecture` client hint says which chip a Mac has.
- * Safari and Firefox report "Intel Mac OS X" on every Mac, so without the
- * hint the page never guesses, and opens "Which Mac do I have?" instead.
+ * An iPhone or iPad is never a Mac. iPhone user agents contain "like Mac OS
+ * X", and iPadOS Safari sends a desktop "Macintosh" user agent by default, so
+ * a "Macintosh" with more than one touch point is an iPad.
*
* Pure, with no imports, so `node --test` loads it directly
* (tests/js/device.test.ts).
@@ -21,15 +15,6 @@
export type DeviceKind = 'mac' | 'ios' | 'other';
-export type MacArch = 'arm64' | 'x86_64';
-
-/** The shape of Chromium's `navigator.userAgentData`, as far as this module uses it. */
-export interface ArchitectureHintSource {
- getHighEntropyValues(hints: string[]): Promise<{ architecture?: string }>;
-}
-
-export type ButtonVariant = 'primary' | 'secondary';
-
/**
* `ios` for an iPhone, iPad or iPod (including iPadOS's desktop user agent),
* `mac` for a Mac, `other` for anything else.
@@ -46,54 +31,6 @@ export function classifyDevice(userAgent: string, maxTouchPoints: number): Devic
return 'other';
}
-/** Chromium's `architecture` value as a build: `arm` → `arm64`, `x86` → `x86_64`, else nothing. */
-export function archFromHint(architecture: string | null | undefined): MacArch | null {
- if (architecture === 'arm') {
- return 'arm64';
- }
-
- if (architecture === 'x86') {
- return 'x86_64';
- }
-
- return null;
-}
-
-/**
- * The Mac's architecture from Chromium's client hint, or null when the browser
- * has no hint (Safari, Firefox), refuses it, or reports something else.
- */
-export async function archHint(source: ArchitectureHintSource | null | undefined): Promise {
- if (!source || typeof source.getHighEntropyValues !== 'function') {
- return null;
- }
-
- try {
- const hints = await source.getHighEntropyValues(['architecture']);
-
- return archFromHint(hints?.architecture);
- } catch {
- return null;
- }
-}
-
-/**
- * Which variant a Mac build's button takes.
- *
- * Primary only for the build a Mac's own hint names. With no hint, both stay
- * secondary, so neither looks like the answer: an Intel Mac in Safari is
- * exactly the reader the pre-rebuild page sent the wrong build. Both variants
- * have the same dimensions, so the change after hydration shifts nothing.
- */
-export function buildVariant(build: MacArch, device: DeviceKind, hint: MacArch | null): ButtonVariant {
- return device === 'mac' && hint === build ? 'primary' : 'secondary';
-}
-
-// "Which Mac do I have?" opens by itself on a Mac whose browser gave no hint. `undefined` is a hint still pending.
-export function chipHelpOpen(device: DeviceKind | null, hint: MacArch | null | undefined): boolean {
- return device === 'mac' && hint === null;
-}
-
/** The App Store card leads on an iPhone or iPad, and only there. */
export function appStoreFirst(device: DeviceKind): boolean {
return device === 'ios';
diff --git a/resources/js/pages/Download.tsx b/resources/js/pages/Download.tsx
index e723f6f7..44cf0c64 100644
--- a/resources/js/pages/Download.tsx
+++ b/resources/js/pages/Download.tsx
@@ -15,8 +15,9 @@ import MacCard, { UI_TAGS } from '@/components/download/mac-card';
import type { DownloadPageProps } from '@/components/download/types';
import { LOCALES, Trans, useI18n, type Messages } from '@/i18n';
import { joinList, keepTogether } from '@/i18n/format';
+import { architecturesText } from '@/lib/data/platforms';
import { PRICING } from '@/lib/data/pricing';
-import { appStoreFirst, archHint, classifyDevice, type DeviceKind, type MacArch } from '@/lib/device';
+import { appStoreFirst, classifyDevice, type DeviceKind } from '@/lib/device';
import { absoluteUrl, graph, iosAppNode, macAppId, macAppNode, webPageNode, type JsonLdGraph } from '@/lib/structured-data';
import LandingLayout from '@/layouts/landing-layout';
@@ -46,10 +47,6 @@ function downloadJsonLd({ baseUrl, inLanguage, m, fmt, path, content, release, m
const pageUrl = absoluteUrl(baseUrl, path('/download'));
const deviceNames = [...mac.deviceNames, ...(ios?.deviceNames ?? [])];
const live = release.source !== 'unavailable' && release.version !== null;
- const architectures = joinList(
- (Object.keys(release.assets) as MacArch[]).map((arch) => m.platforms.architectures[arch]),
- { separator: ', ', last: m.platforms.architectures.joiner },
- );
return graph([
webPageNode(context, {
@@ -69,7 +66,7 @@ function downloadJsonLd({ baseUrl, inLanguage, m, fmt, path, content, release, m
alternateName: m.seo.macApp.alternateName,
subCategory: m.seo.macApp.subCategory,
operatingSystem: mac.requirements.systems.join(', '),
- requirements: `${requirementLine(mac.requirements, m.platforms)}, ${architectures}`,
+ requirements: `${requirementLine(mac.requirements, m.platforms)}, ${architecturesText(m.platforms)}`,
licenseUrl: links.license,
version: live ? release.version : null,
downloadUrl: pageUrl,
@@ -91,41 +88,22 @@ function downloadJsonLd({ baseUrl, inLanguage, m, fmt, path, content, release, m
/**
* `/download` and `/vi/download` (sitemap §A.1, design-system §8.7).
*
- * The server renders everything a reader needs: both Mac builds with their
- * real DMG URLs, the Homebrew command and the App Store card. After mount the
- * page reads the device only to adjust emphasis (resources/js/lib/device.ts):
- * on an iPhone or iPad the App Store card comes first and neither Mac build is
- * highlighted; on a Mac whose Chromium browser reports its architecture, that
- * build's button becomes primary. Nothing starts a download on its own, and no
- * copy says a download started.
+ * The server renders everything a reader needs: the universal DMG with its
+ * real URL, the Homebrew command and the App Store card. After mount the page
+ * reads the device only to adjust emphasis (resources/js/lib/device.ts): on an
+ * iPhone or iPad the App Store card comes first. Nothing starts a download on
+ * its own, and no copy says a download started.
*
- * If no release source has ever answered, both buttons open GitHub's latest
+ * If no release source has ever answered, the button opens GitHub's latest
* release, no version is shown, and the Mac card says why.
*/
export default function Download({ content, release, mac, ios, unreleased, links, featuredEngines, latestPost }: DownloadPageProps) {
const { canonicalBaseUrl } = usePage().props;
const { locale, m, fmt, path } = useI18n();
const [device, setDevice] = useState(null);
- // Undefined until the browser has answered; only a Mac is asked.
- const [hint, setHint] = useState(undefined);
useEffect(() => {
- let cancelled = false;
- const kind = classifyDevice(navigator.userAgent, navigator.maxTouchPoints ?? 0);
-
- setDevice(kind);
-
- if (kind === 'mac') {
- void archHint(navigator.userAgentData).then((arch) => {
- if (!cancelled) {
- setHint(arch);
- }
- });
- }
-
- return () => {
- cancelled = true;
- };
+ setDevice(classifyDevice(navigator.userAgent, navigator.maxTouchPoints ?? 0));
}, []);
const available = release.source !== 'unavailable' && release.version !== null;
@@ -136,7 +114,7 @@ export default function Download({ content, release, mac, ios, unreleased, links
.map((id) => m.platforms.names[id]);
const macCard = (
-
+
);
const iosCard = ios !== null ? : null;
diff --git a/resources/js/types/globals.d.ts b/resources/js/types/globals.d.ts
index 7f60d131..9126510e 100644
--- a/resources/js/types/globals.d.ts
+++ b/resources/js/types/globals.d.ts
@@ -2,22 +2,6 @@
* Ambient declarations for APIs the DOM lib does not cover.
*/
-/**
- * User-Agent Client Hints. Chromium-only and still not in TypeScript's DOM lib,
- * so it is declared here rather than cast away at each call site. Optional
- * because Safari and Firefox do not implement it — the download page falls back
- * to the user-agent string when it is absent.
- *
- * @see https://wicg.github.io/ua-client-hints/
- */
-interface NavigatorUAData {
- getHighEntropyValues(hints: string[]): Promise<{ architecture?: string; platform?: string }>;
-}
-
-interface Navigator {
- userAgentData?: NavigatorUAData;
-}
-
/**
* The SSR bundle runs under Node, where process.env is how the port is
* configured. Declared minimally rather than pulling @types/node in, which
diff --git a/tests/Feature/Data/PlatformsDataTest.php b/tests/Feature/Data/PlatformsDataTest.php
index f0d36c69..e94e6751 100644
--- a/tests/Feature/Data/PlatformsDataTest.php
+++ b/tests/Feature/Data/PlatformsDataTest.php
@@ -95,20 +95,19 @@ function platformsDataEntry(string $id): array
expect(preg_match('/macOS\s*14|"minVersion":\s*"14/', $raw))->toBe(0);
});
-it('ships separate Apple silicon and Intel builds, never a universal binary', function (): void {
+it('ships one universal DMG for Apple silicon and Intel', function (): void {
$mac = platformsDataEntry('mac');
- expect($mac['universalBinary'])->toBeFalse();
- expect(array_column($mac['architectures'], 'id'))->toBe(['arm64', 'x86_64']);
-
- foreach ($mac['architectures'] as $architecture) {
- expect($architecture['assetTemplate'])->toBe("TablePro-{version}-{$architecture['id']}.dmg");
- }
+ expect($mac['universalBinary'])->toBeTrue();
+ expect($mac['architectures'])->toBe(['arm64', 'x86_64']);
+ expect($mac['assetTemplate'])->toBe('TablePro-{version}.dmg');
$destinations = collect($mac['destinations'])->keyBy('kind');
expect($destinations->keys()->all())->toBe(['dmg', 'homebrew', 'releases']);
- expect($destinations['dmg']['urlTemplate'])->toContain('{version}')->toContain('{arch}');
+ expect($destinations['dmg']['urlTemplate'])
+ ->toBe('https://github.com/TableProApp/TablePro/releases/download/v{version}/' . $mac['assetTemplate'])
+ ->not->toContain('{arch}');
expect($destinations['homebrew']['command'])->toBe('brew install --cask tablepro');
expect($destinations['releases']['url'])->toBe('https://github.com/TableProApp/TablePro/releases');
});
diff --git a/tests/Feature/Releases/DownloadPageTest.php b/tests/Feature/Releases/DownloadPageTest.php
index 6c67ae2c..add238dd 100644
--- a/tests/Feature/Releases/DownloadPageTest.php
+++ b/tests/Feature/Releases/DownloadPageTest.php
@@ -45,9 +45,9 @@ function fakeLiveRelease(): void
->where('release.source', 'github')
->where('release.version', '0.77.0')
->where('release.publishedAtFormatted', 'October 2, 2026')
- ->where('release.assets.arm64.url', 'https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0-arm64.dmg')
- ->where('release.assets.arm64.bytes', 22_943_352)
- ->where('release.assets.x86_64.url', 'https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0-x86_64.dmg')
+ ->where('release.asset.name', 'TablePro-0.77.0.dmg')
+ ->where('release.asset.url', 'https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0.dmg')
+ ->where('release.asset.bytes', 65_432_179)
->where('mac.requirements.minVersion', '13.0')
->where('mac.requirements.releaseName', 'Ventura')
->where('mac.homebrewCommand', 'brew install --cask tablepro')
@@ -188,9 +188,8 @@ function downloadNewestReleasePost(): array
->where('release.source', 'unavailable')
->where('release.version', null)
->where('release.publishedAtFormatted', null)
- ->where('release.assets.arm64.url', 'https://github.com/TableProApp/TablePro/releases/latest')
- ->where('release.assets.x86_64.url', 'https://github.com/TableProApp/TablePro/releases/latest')
- ->where('release.assets.arm64.name', null)
+ ->where('release.asset.url', 'https://github.com/TableProApp/TablePro/releases/latest')
+ ->where('release.asset.name', null)
->where('mac.homebrewTrails', false));
});
@@ -248,15 +247,16 @@ function downloadNewestReleasePost(): array
expect(preg_match('/chậm hơn|dòng Chip|nơi bạn chọn/u', $text))->toBe(0);
});
-it('server-renders both builds as links, says nothing about a download starting, and shows no checksum block without digests', function (): void {
+it('server-renders the universal DMG as a link, says nothing about a download starting, and shows no checksum block without a digest', function (): void {
fakeLiveRelease();
$pages = ['/download' => ssrHtml('/download'), '/vi/download' => ssrHtml('/vi/download')];
foreach ($pages as $html) {
expect($html)
- ->toContain('href="https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0-arm64.dmg"')
- ->toContain('href="https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0-x86_64.dmg"')
+ ->toContain('href="https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0.dmg"')
+ ->not->toContain('arm64.dmg')
+ ->not->toContain('x86_64.dmg')
->toContain('href="https://apps.apple.com/app/tablepro/id6761621829"')
->toContain('brew install --cask tablepro')
->toContain('id="mac"')
@@ -278,53 +278,46 @@ function downloadNewestReleasePost(): array
}
expect($pages['/download'])->toContain('Requires macOS 13 Ventura or later')
- ->toContain('Download for Apple silicon')
- ->toContain('Download for Intel')
- ->toContain('Which Mac do I have?')
+ ->toContain('Download for Mac')
+ ->not->toContain('Download for Apple silicon')
+ ->not->toContain('Which Mac do I have?')
->toContain('v0.77.0 · October 2, 2026')
- ->toContain('TablePro-0.77.0-arm64.dmg · 22.9 MB')
+ ->toContain('TablePro-0.77.0.dmg · 65.4 MB')
->toContain('Requires iOS and iPadOS 18 or later')
->toContain('Free, with no in-app purchases')
->toContain('not available for Linux or Windows')
->not->toContain('Verify your download')
- ->not->toContain('shasum')
- // The server cannot know the browser, so "Which Mac do I have?" renders closed; chipHelpOpen() opens it (tests/js/device.test.ts).
- ->toMatch('/not->toContain('shasum');
expect($pages['/vi/download'])->toContain('Yêu cầu macOS 13 Ventura trở lên')
- ->toContain('Tải bản cho Apple silicon')
- ->toContain('Tải bản cho Intel')
+ ->toContain('Tải về cho Mac')
->toContain('v0.77.0 · 2 tháng 10 năm 2026')
- ->toContain('TablePro-0.77.0-arm64.dmg · 22,9 MB')
+ ->toContain('TablePro-0.77.0.dmg · 65,4 MB')
->toContain('Yêu cầu iOS và iPadOS 18 trở lên')
->toContain('Miễn phí, không có mua hàng trong ứng dụng')
->toContain('cho Linux hoặc Windows');
})->group('ssr');
-it('sends each DMG’s SHA-256 to the page when the release carries one', function (): void {
- $arm64 = str_repeat('a1', 32);
- $x86_64 = str_repeat('b2', 32);
+it('sends the DMG’s SHA-256 to the page when the release carries one', function (): void {
+ $sha256 = str_repeat('a1', 32);
- Http::fake([RELEASES_FAKE_GITHUB_LATEST => Http::response(githubReleasePayloadWithDigests($arm64, $x86_64))]);
+ Http::fake([RELEASES_FAKE_GITHUB_LATEST => Http::response(githubReleasePayloadWithDigest($sha256))]);
Artisan::call('release:refresh');
- get('/download')->assertInertia(fn(AssertableInertia $page) => $page
- ->where('release.assets.arm64.sha256', $arm64)
- ->where('release.assets.x86_64.sha256', $x86_64));
+ get('/download')->assertInertia(fn(AssertableInertia $page) => $page->where('release.asset.sha256', $sha256));
});
-it('server-renders each build’s SHA-256 with the command that checks it, in English and a translated page', function (): void {
- $arm64 = str_repeat('a1', 32);
- $x86_64 = str_repeat('b2', 32);
+it('server-renders the DMG’s SHA-256 with the command that checks it, in English and a translated page', function (): void {
+ $sha256 = str_repeat('a1', 32);
- Http::fake([RELEASES_FAKE_GITHUB_LATEST => Http::response(githubReleasePayloadWithDigests($arm64, $x86_64))]);
+ Http::fake([RELEASES_FAKE_GITHUB_LATEST => Http::response(githubReleasePayloadWithDigest($sha256))]);
Artisan::call('release:refresh');
- expect(ssrHtml('/download'))->toContain('Verify your download', $arm64, $x86_64, 'shasum -a 256');
- expect(ssrHtml('/vi/download'))->toContain($arm64, $x86_64, 'shasum -a 256')->not->toContain('Verify your download');
+ expect(ssrHtml('/download'))->toContain('Verify your download', 'TablePro-0.77.0.dmg', $sha256, 'shasum -a 256');
+ expect(ssrHtml('/vi/download'))->toContain($sha256, 'shasum -a 256')->not->toContain('Verify your download');
})->group('ssr');
-it('server-renders no version and sends both buttons to the latest release when no source answers', function (): void {
+it('server-renders no version and sends the button to the latest release when no source answers', function (): void {
Http::fake([
RELEASES_FAKE_GITHUB_LATEST => Http::response('', 500),
RELEASES_FAKE_APPCAST => Http::response('', 500),
@@ -332,8 +325,8 @@ function downloadNewestReleasePost(): array
$html = ssrHtml('/download');
- expect(substr_count($html, 'href="https://github.com/TableProApp/TablePro/releases/latest"'))->toBeGreaterThanOrEqual(2)
- ->and($html)->toContain('Release details are unavailable. Both buttons open the latest release on GitHub; choose a build there.')
+ expect(substr_count($html, 'href="https://github.com/TableProApp/TablePro/releases/latest"'))->toBeGreaterThanOrEqual(1)
+ ->and($html)->toContain('Release details are unavailable. The button opens the latest release on GitHub.')
->not->toContain('softwareVersion')
->not->toContain('.dmg"');
})->group('ssr');
diff --git a/tests/Feature/Releases/MacReleaseServiceTest.php b/tests/Feature/Releases/MacReleaseServiceTest.php
index bf876c2c..d295ce9c 100644
--- a/tests/Feature/Releases/MacReleaseServiceTest.php
+++ b/tests/Feature/Releases/MacReleaseServiceTest.php
@@ -46,7 +46,7 @@ function githubApiCalls(): int
return Http::recorded(fn(Request $request): bool => str_contains($request->url(), 'api.github.com'))->count();
}
-it('reads the app release from releases/latest, with both DMGs and their sizes', function (): void {
+it('reads the app release from releases/latest, with the universal DMG and its size', function (): void {
Http::fake([RELEASES_FAKE_GITHUB_LATEST => Http::response(githubReleasePayload())]);
$release = macRelease();
@@ -54,14 +54,12 @@ function githubApiCalls(): int
expect($release->source)->toBe(MacRelease::SOURCE_GITHUB)
->and($release->version)->toBe('0.77.0')
->and($release->publishedAt)->toBe('2026-10-02')
- ->and($release->assets['arm64'])->toBe([
- 'name' => 'TablePro-0.77.0-arm64.dmg',
- 'url' => 'https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0-arm64.dmg',
- 'bytes' => 22_943_352,
+ ->and($release->asset)->toBe([
+ 'name' => 'TablePro-0.77.0.dmg',
+ 'url' => 'https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0.dmg',
+ 'bytes' => 65_432_179,
'sha256' => null,
])
- ->and($release->assets['x86_64']['name'])->toBe('TablePro-0.77.0-x86_64.dmg')
- ->and($release->assets['x86_64']['bytes'])->toBe(26_202_607)
->and($release->releaseUrl)->toBe('https://github.com/TableProApp/TablePro/releases/tag/v0.77.0')
->and($release->releasesUrl)->toBe('https://github.com/TableProApp/TablePro/releases');
@@ -69,32 +67,27 @@ function githubApiCalls(): int
Http::assertNotSent(fn(Request $request): bool => str_contains($request->url(), '/releases?') || str_ends_with($request->url(), '/releases'));
});
-it('keeps each DMG’s SHA-256 when the releases API reports one, in the cache and in the last good copy', function (): void {
- $arm64 = str_repeat('a1', 32);
+it('keeps the DMG’s SHA-256 when the releases API reports one, in the cache and in the last good copy', function (): void {
+ $sha256 = str_repeat('a1', 32);
- Http::fake([RELEASES_FAKE_GITHUB_LATEST => Http::response(githubReleasePayload(assets: [
- githubAsset('TablePro-0.77.0-arm64.dmg', 22_943_352, digest: "sha256:{$arm64}"),
- githubAsset('TablePro-0.77.0-x86_64.dmg', 26_202_607),
- ]))]);
+ Http::fake([RELEASES_FAKE_GITHUB_LATEST => Http::response(githubReleasePayloadWithDigest($sha256))]);
$release = macRelease();
- expect($release->assets['arm64']['sha256'])->toBe($arm64)
- ->and($release->assets['x86_64']['sha256'])->toBeNull()
- ->and($release->toProps('en')['assets']['arm64']['sha256'])->toBe($arm64)
- ->and(releases()->lastGood()?->assets['arm64']['sha256'])->toBe($arm64);
+ expect($release->asset['sha256'])->toBe($sha256)
+ ->and($release->toProps('en')['asset']['sha256'])->toBe($sha256)
+ ->and(releases()->lastGood()?->asset['sha256'])->toBe($sha256);
});
it('states no checksum for a digest that is not a SHA-256', function (mixed $digest): void {
Http::fake([RELEASES_FAKE_GITHUB_LATEST => Http::response(githubReleasePayload(assets: [
- [...githubAsset('TablePro-0.77.0-arm64.dmg', 22_943_352), 'digest' => $digest],
- githubAsset('TablePro-0.77.0-x86_64.dmg', 26_202_607),
+ [...githubAsset('TablePro-0.77.0.dmg', 65_432_179), 'digest' => $digest],
]))]);
$release = macRelease();
expect($release->source)->toBe(MacRelease::SOURCE_GITHUB)
- ->and($release->assets['arm64']['sha256'])->toBeNull();
+ ->and($release->asset['sha256'])->toBeNull();
})->with([
'another algorithm' => ['sha512:' . str_repeat('ab', 64)],
'too short' => ['sha256:abc123'],
@@ -103,19 +96,31 @@ function githubApiCalls(): int
'not a string' => [42],
]);
-it('reads a copy stored before checksums were kept', function (): void {
- $asset = ['name' => 'TablePro-0.77.0-arm64.dmg', 'url' => 'https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0-arm64.dmg', 'bytes' => 1];
-
+it('reads a copy stored without a checksum', function (): void {
$release = MacRelease::fromArray([
'version' => '0.77.0',
'publishedAt' => '2026-10-02',
- 'assets' => ['arm64' => $asset, 'x86_64' => $asset],
+ 'asset' => ['name' => 'TablePro-0.77.0.dmg', 'url' => 'https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0.dmg', 'bytes' => 1],
'releaseUrl' => 'https://github.com/TableProApp/TablePro/releases/tag/v0.77.0',
'releasesUrl' => 'https://github.com/TableProApp/TablePro/releases',
'source' => MacRelease::SOURCE_GITHUB,
]);
- expect($release?->assets['arm64']['sha256'])->toBeNull();
+ expect($release?->asset['sha256'])->toBeNull();
+});
+
+// The copy stored before 0.82.0 pointed at a DMG per architecture; it is refreshed, never served.
+it('ignores a copy stored with a DMG per architecture', function (): void {
+ $asset = ['name' => 'TablePro-0.81.0-arm64.dmg', 'url' => 'https://github.com/TableProApp/TablePro/releases/download/v0.81.0/TablePro-0.81.0-arm64.dmg', 'bytes' => 1, 'sha256' => null];
+
+ expect(MacRelease::fromArray([
+ 'version' => '0.81.0',
+ 'publishedAt' => '2026-10-16',
+ 'assets' => ['arm64' => $asset, 'x86_64' => $asset],
+ 'releaseUrl' => 'https://github.com/TableProApp/TablePro/releases/tag/v0.81.0',
+ 'releasesUrl' => 'https://github.com/TableProApp/TablePro/releases',
+ 'source' => MacRelease::SOURCE_GITHUB,
+ ]))->toBeNull();
});
it('formats the release date for the reader in PHP, in each locale', function (): void {
@@ -142,16 +147,20 @@ function githubApiCalls(): int
->and($release->version)->toBe('0.77.0');
});
-it('rejects an app release missing one architecture', function (): void {
+it('rejects an app release without the universal DMG', function (array $assets): void {
Http::fake([
- RELEASES_FAKE_GITHUB_LATEST => Http::response(githubReleasePayload(assets: [
- githubAsset('TablePro-0.77.0-arm64.dmg', 22_943_352),
- ])),
+ RELEASES_FAKE_GITHUB_LATEST => Http::response(githubReleasePayload(assets: $assets)),
RELEASES_FAKE_APPCAST => Http::response(appcastXml('0.76.1')),
]);
expect(macRelease())->source->toBe(MacRelease::SOURCE_APPCAST)->version->toBe('0.76.1');
-});
+})->with([
+ 'a DMG per architecture' => [[
+ githubAsset('TablePro-0.77.0-arm64.dmg', 22_943_352),
+ githubAsset('TablePro-0.77.0-x86_64.dmg', 26_202_607),
+ ]],
+ 'only the update archive' => [[githubAsset('TablePro-0.77.0.tar.xz', 41_754_460)]],
+]);
it('rejects an asset still uploading, a draft and a prerelease', function (array $payload): void {
Http::fake([
@@ -162,15 +171,14 @@ function githubApiCalls(): int
expect(macRelease()->source)->toBe(MacRelease::SOURCE_APPCAST);
})->with([
'uploading' => [githubReleasePayload(assets: [
- githubAsset('TablePro-0.77.0-arm64.dmg', 22_943_352),
- [...githubAsset('TablePro-0.77.0-x86_64.dmg', 0), 'state' => 'new'],
+ [...githubAsset('TablePro-0.77.0.dmg', 0), 'state' => 'new'],
])],
'draft' => [githubReleasePayload(overrides: ['draft' => true])],
'prerelease' => [githubReleasePayload(overrides: ['prerelease' => true])],
'not a version tag' => [githubReleasePayload('nightly')],
]);
-it('builds the DMG links from platforms.json when it falls back to the appcast', function (): void {
+it('builds the DMG link from platforms.json when it falls back to the appcast', function (): void {
Http::fake([
RELEASES_FAKE_GITHUB_LATEST => Http::response(['message' => 'API rate limit exceeded'], 403),
RELEASES_FAKE_APPCAST => Http::response(appcastXml('0.77.0', 'Fri, 02 Oct 2026 04:15:25 +0000')),
@@ -179,20 +187,20 @@ function githubApiCalls(): int
$release = macRelease();
expect($release->source)->toBe(MacRelease::SOURCE_APPCAST)
+ ->and($release->version)->toBe('0.77.0')
->and($release->publishedAt)->toBe('2026-10-02')
- ->and($release->assets['arm64'])->toBe([
- 'name' => 'TablePro-0.77.0-arm64.dmg',
- 'url' => 'https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0-arm64.dmg',
+ ->and($release->asset)->toBe([
+ 'name' => 'TablePro-0.77.0.dmg',
+ 'url' => 'https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0.dmg',
'bytes' => null,
'sha256' => null,
])
- ->and($release->assets['x86_64']['url'])->toBe('https://github.com/TableProApp/TablePro/releases/download/v0.77.0/TablePro-0.77.0-x86_64.dmg')
->and($release->releaseUrl)->toBe('https://github.com/TableProApp/TablePro/releases/tag/v0.77.0');
});
-it('takes the fallback DMG links from the dmg destination’s urlTemplate when platforms.json has one', function (): void {
+it('takes the fallback DMG link from the dmg destination’s urlTemplate when platforms.json has one', function (): void {
bindReleaseFixturePlatforms(['mac.destinations' => [
- ['kind' => 'dmg', 'urlTemplate' => 'https://mirror.example/tablepro/v{version}/TablePro-{version}-{arch}.dmg'],
+ ['kind' => 'dmg', 'urlTemplate' => 'https://mirror.example/tablepro/v{version}/TablePro-{version}.dmg'],
['kind' => 'releases', 'url' => 'https://github.com/TableProApp/TablePro/releases'],
]]);
Http::fake([
@@ -203,8 +211,7 @@ function githubApiCalls(): int
$release = macRelease();
expect($release->source)->toBe(MacRelease::SOURCE_APPCAST)
- ->and($release->assets['arm64']['url'])->toBe('https://mirror.example/tablepro/v0.77.0/TablePro-0.77.0-arm64.dmg')
- ->and($release->assets['x86_64']['url'])->toBe('https://mirror.example/tablepro/v0.77.0/TablePro-0.77.0-x86_64.dmg');
+ ->and($release->asset['url'])->toBe('https://mirror.example/tablepro/v0.77.0/TablePro-0.77.0.dmg');
});
it('answers a page from what the last refresh stored, without calling anything', function (): void {
@@ -246,10 +253,7 @@ function githubApiCalls(): int
it('serves the last good copy while the stored one is missing, and refreshes after the response', function (): void {
Http::fakeSequence(RELEASES_FAKE_GITHUB_LATEST)
- ->push(githubReleasePayload('v0.76.1', assets: [
- githubAsset('TablePro-0.76.1-arm64.dmg', 1, 'v0.76.1'),
- githubAsset('TablePro-0.76.1-x86_64.dmg', 2, 'v0.76.1'),
- ]))
+ ->push(githubReleasePayload('v0.76.1'))
->push(githubReleasePayload());
expect(macRelease()->version)->toBe('0.76.1');
@@ -294,10 +298,7 @@ function githubApiCalls(): int
it('serves the last good copy when both sources fail, and pages stop asking for 15 minutes', function (): void {
Http::fakeSequence(RELEASES_FAKE_GITHUB_LATEST)
- ->push(githubReleasePayload('v0.76.1', assets: [
- githubAsset('TablePro-0.76.1-arm64.dmg', 1, 'v0.76.1'),
- githubAsset('TablePro-0.76.1-x86_64.dmg', 2, 'v0.76.1'),
- ]))
+ ->push(githubReleasePayload('v0.76.1'))
->whenEmpty(Http::response('', 502));
Http::fake([RELEASES_FAKE_APPCAST => Http::response('', 503)]);
@@ -308,7 +309,7 @@ function githubApiCalls(): int
$stale = servePage();
expect($stale->version)->toBe('0.76.1')
- ->and($stale->assets['arm64']['url'])->toEndWith('/TablePro-0.76.1-arm64.dmg')
+ ->and($stale->asset['url'])->toEndWith('/TablePro-0.76.1.dmg')
->and(releases()->failing())->toBeTrue();
$calls = githubApiCalls();
@@ -357,7 +358,7 @@ function githubApiCalls(): int
expect($release->source)->toBe(MacRelease::SOURCE_GITHUB)
->and($release->version)->toBe('0.77.0')
- ->and($release->assets['arm64']['url'])->toEndWith('/TablePro-0.77.0-arm64.dmg')
+ ->and($release->asset['url'])->toEndWith('/TablePro-0.77.0.dmg')
->and(Storage::disk(MacReleaseService::LAST_GOOD_DISK)->exists(MacReleaseService::LAST_GOOD_FILE))->toBeTrue();
});
@@ -371,7 +372,7 @@ function githubApiCalls(): int
expect(macRelease()->source)->toBe(MacRelease::SOURCE_UNAVAILABLE);
});
-it('is unavailable when nothing ever answered: no version, and both buttons open the latest release page', function (): void {
+it('is unavailable when nothing ever answered: no version, and the button opens the latest release page', function (): void {
Http::fake([
RELEASES_FAKE_GITHUB_LATEST => Http::failedConnection(),
RELEASES_FAKE_APPCAST => Http::failedConnection(),
@@ -383,8 +384,7 @@ function githubApiCalls(): int
->and($release->isAvailable())->toBeFalse()
->and($release->version)->toBeNull()
->and($release->publishedAt)->toBeNull()
- ->and($release->assets['arm64'])->toBe(['name' => null, 'url' => 'https://github.com/TableProApp/TablePro/releases/latest', 'bytes' => null, 'sha256' => null])
- ->and($release->assets['x86_64']['url'])->toBe('https://github.com/TableProApp/TablePro/releases/latest')
+ ->and($release->asset)->toBe(['name' => null, 'url' => 'https://github.com/TableProApp/TablePro/releases/latest', 'bytes' => null, 'sha256' => null])
->and($release->toProps('en')['publishedAtFormatted'])->toBeNull();
});
@@ -422,10 +422,7 @@ function githubApiCalls(): int
it('fails the scheduled refresh when no source answers, and keeps serving the last good copy', function (): void {
Http::fakeSequence(RELEASES_FAKE_GITHUB_LATEST)
- ->push(githubReleasePayload('v0.76.1', assets: [
- githubAsset('TablePro-0.76.1-arm64.dmg', 1, 'v0.76.1'),
- githubAsset('TablePro-0.76.1-x86_64.dmg', 2, 'v0.76.1'),
- ]))
+ ->push(githubReleasePayload('v0.76.1'))
->whenEmpty(Http::response('', 502));
Http::fake([RELEASES_FAKE_APPCAST => Http::response('', 503)]);
@@ -461,7 +458,7 @@ function githubApiCalls(): int
expect(githubApiCalls())->toBe(0);
});
-it('reads only the top appcast item, and ignores a feed it cannot parse', function (): void {
+it('reads the stable appcast item, skipping a newer beta above it, and ignores a feed it cannot parse', function (): void {
expect(SparkleAppcast::parse(appcastXml('0.77.0', 'Fri, 02 Oct 2026 04:15:25 +0000', '13.0')))->toBe([
'version' => '0.77.0',
'publishedAt' => '2026-10-02',
@@ -474,7 +471,17 @@ function githubApiCalls(): int
->and(SparkleAppcast::parse('latest'))->toBeNull();
});
+it('picks the stable item with the highest build, wherever it sits in the feed', function (): void {
+ $item = fn(string $version, int $build, string $extra = ''): string => "{$version}{$build}{$version}{$extra}";
+ $feed = fn(string ...$items): string => '' . implode('', $items) . '';
+
+ // A patch on an older line can be published after the newer stable, and lands above it.
+ expect(SparkleAppcast::parse($feed($item('0.81.1', 810199), $item('0.82.0', 820099), $item('0.81.0', 810099)))['version'] ?? null)->toBe('0.82.0')
+ ->and(SparkleAppcast::parse($feed($item('0.83.0-beta.1', 830001, 'beta'), $item('0.82.0', 820099)))['version'] ?? null)->toBe('0.82.0')
+ ->and(SparkleAppcast::parse($feed($item('0.83.0', 830099, 'beta'))))->toBeNull()
+ ->and(SparkleAppcast::parse($feed($item('0.82.0', 820099), "0.83.00.83.0"))['version'] ?? null)->toBe('0.82.0');
+});
+
it('reads the appcast from the feed the app itself uses', function (): void {
- expect(app(SparkleAppcast::class)->feedUrl())
- ->toBe('https://raw.githubusercontent.com/TableProApp/TablePro/main/appcast.xml');
+ expect(app(SparkleAppcast::class)->feedUrl())->toBe('https://updates.tablepro.app/appcast.xml');
});
diff --git a/tests/Feature/Releases/ReleaseFixtures.php b/tests/Feature/Releases/ReleaseFixtures.php
index 23985b03..abfdc2c7 100644
--- a/tests/Feature/Releases/ReleaseFixtures.php
+++ b/tests/Feature/Releases/ReleaseFixtures.php
@@ -5,7 +5,7 @@
const RELEASES_FAKE_GITHUB_LATEST = 'api.github.com/repos/TableProApp/TablePro/releases/latest';
-const RELEASES_FAKE_APPCAST = 'raw.githubusercontent.com/TableProApp/TablePro/main/appcast.xml';
+const RELEASES_FAKE_APPCAST = 'updates.tablepro.app/appcast.xml';
/**
* @param array $overrides
@@ -20,11 +20,9 @@ function bindReleaseFixturePlatforms(array $overrides = []): array
'status' => 'released',
'deviceNames' => ['Mac'],
'requirements' => ['systems' => ['macOS'], 'minVersion' => '13.0', 'displayVersion' => '13', 'releaseName' => 'Ventura'],
- 'architectures' => [
- ['id' => 'arm64', 'assetTemplate' => 'TablePro-{version}-arm64.dmg'],
- ['id' => 'x86_64', 'assetTemplate' => 'TablePro-{version}-x86_64.dmg'],
- ],
- 'universalBinary' => false,
+ 'architectures' => ['arm64', 'x86_64'],
+ 'universalBinary' => true,
+ 'assetTemplate' => 'TablePro-{version}.dmg',
'destinations' => [
['kind' => 'dmg'],
['kind' => 'homebrew', 'command' => 'brew install --cask tablepro'],
@@ -79,10 +77,10 @@ function githubReleasePayload(string $tag = 'v0.77.0', array $overrides = [], ?a
'html_url' => "https://github.com/TableProApp/TablePro/releases/tag/{$tag}",
'published_at' => '2026-10-02T04:15:45Z',
'assets' => $assets ?? [
- githubAsset("TablePro-{$version}-arm64.dmg", 22_943_352),
- githubAsset("TablePro-{$version}-x86_64.dmg", 26_202_607),
- githubAsset("TablePro-{$version}-arm64.zip", 31_475_263),
- githubAsset("TablePro-{$version}-x86_64.zip", 33_766_590),
+ githubAsset("OracleDriver-{$version}.zip", 10_224_221, $tag),
+ githubAsset("TablePro-{$version}-dSYMs.zip", 182_735_436, $tag),
+ githubAsset("TablePro-{$version}.dmg", 65_432_179, $tag),
+ githubAsset("TablePro-{$version}.tar.xz", 41_754_460, $tag),
],
...$overrides,
];
@@ -105,42 +103,57 @@ function githubAsset(string $name, int $size, string $tag = 'v0.77.0', ?string $
/**
* @return array
*/
-function githubReleasePayloadWithDigests(string $arm64, string $x86_64): array
+function githubReleasePayloadWithDigest(string $sha256): array
{
return githubReleasePayload(assets: [
- githubAsset('TablePro-0.77.0-arm64.dmg', 22_943_352, digest: "sha256:{$arm64}"),
- githubAsset('TablePro-0.77.0-x86_64.dmg', 26_202_607, digest: "sha256:{$x86_64}"),
+ githubAsset('TablePro-0.77.0.dmg', 65_432_179, digest: "sha256:{$sha256}"),
]);
}
+/**
+ * The app's build number for a stable version: `0.82.0` is 820099.
+ */
+function appcastBuild(string $version): int
+{
+ [$major, $minor, $patch] = array_map('intval', explode('.', $version . '.0.0'));
+
+ return (($major * 1000 + $minor) * 100 + $patch) * 100 + 99;
+}
+
+/**
+ * The feed as the release workflow writes it: a newer beta on top, then the
+ * stable `$version`, then an older stable.
+ */
function appcastXml(string $version = '0.77.0', string $pubDate = 'Fri, 02 Oct 2026 04:15:25 +0000', string $minimum = '13.0'): string
{
+ $build = appcastBuild($version);
+
return <<TablePro
- {$version}
- {$pubDate}
- 134
- {$version}
- {$minimum}
- arm64
-
+ 0.99.0-beta.1
+ Sat, 03 Oct 2026 04:15:25 +0000
+ beta
+ 990001
+ 0.99.0-beta.1
+ 14.0
+ {$version}{$pubDate}
- 134
+ {$build}{$version}{$minimum}
-
+ 0.76.1Mon, 28 Sep 2026 22:46:49 +0000
- 133
+ 7601990.76.113.0
diff --git a/tests/js/compare-model.test.ts b/tests/js/compare-model.test.ts
index adfde001..35908431 100644
--- a/tests/js/compare-model.test.ts
+++ b/tests/js/compare-model.test.ts
@@ -52,6 +52,7 @@ const facts: TableProFacts = {
macRequirement: 'macOS 13 Ventura or later',
iosRequirement: 'iOS and iPadOS 18 or later',
macArchitectures: ['arm64', 'x86_64'],
+ macUniversal: true,
starter: { ...pricing.tiers.starter.prices, activations: pricing.tiers.starter.activations },
team: { ...pricing.tiers.team.prices, minSeats: pricing.tiers.team.seats.min },
iosFree: true,
@@ -228,5 +229,14 @@ test('calls a single app for both architectures universal, and keeps "separate b
}
assert.ok(lines('dbeaver').includes(hub.en.labels.builds.both), 'DBeaver ships a DMG per architecture');
+
+ const ours = (macUniversal: boolean) =>
+ glanceRows(product('dbeaver'), comparisons.rows, { ...facts, macUniversal }, context('en'))
+ .find((row) => row.key === 'platforms')
+ ?.tablepro?.lines.map((line) => line.text) ?? [];
+
+ assert.ok(ours(true).includes(hub.en.labels.builds.universal), 'TablePro ships one universal DMG');
+ assert.ok(!ours(true).includes(hub.en.labels.builds.both));
+ assert.ok(ours(false).includes(hub.en.labels.builds.both));
});
diff --git a/tests/js/device.test.ts b/tests/js/device.test.ts
index 6c175608..88e055bc 100644
--- a/tests/js/device.test.ts
+++ b/tests/js/device.test.ts
@@ -2,15 +2,7 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
-import {
- appStoreFirst,
- archFromHint,
- archHint,
- buildVariant,
- chipHelpOpen,
- classifyDevice,
- type ArchitectureHintSource,
-} from '../../resources/js/lib/device.ts';
+import { appStoreFirst, classifyDevice } from '../../resources/js/lib/device.ts';
/*
* The download page's device rules (architecture §1.13). Nothing here may make
@@ -30,10 +22,6 @@ const UA = {
linux: 'Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0',
};
-function hints(architecture: string | undefined): ArchitectureHintSource {
- return { getHighEntropyValues: async () => ({ architecture }) };
-}
-
test('an iPhone is never a Mac, although its user agent says "like Mac OS X"', () => {
assert.equal(classifyDevice(UA.iPhone, 5), 'ios');
assert.equal(classifyDevice(UA.iPhone, 0), 'ios');
@@ -60,73 +48,12 @@ test('Windows, Linux and Android are neither', () => {
assert.equal(classifyDevice('', 0), 'other');
});
-test('the Chromium architecture hint maps to a build, and nothing else does', () => {
- assert.equal(archFromHint('arm'), 'arm64');
- assert.equal(archFromHint('x86'), 'x86_64');
- assert.equal(archFromHint(''), null);
- assert.equal(archFromHint('ARM'), null);
- assert.equal(archFromHint(undefined), null);
- assert.equal(archFromHint(null), null);
-});
-
-test('Intel Chrome reports x86 and Apple silicon Chrome reports arm', async () => {
- assert.equal(await archHint(hints('x86')), 'x86_64');
- assert.equal(await archHint(hints('arm')), 'arm64');
-});
-
-test('Safari and Firefox have no hint, so the page does not guess', async () => {
- assert.equal(await archHint(undefined), null);
- assert.equal(await archHint(null), null);
- assert.equal(await archHint({} as ArchitectureHintSource), null);
- assert.equal(await archHint(hints(undefined)), null);
-});
-
-test('a refused or failing hint is no hint', async () => {
- const failing: ArchitectureHintSource = {
- getHighEntropyValues: async () => {
- throw new Error('NotAllowedError');
- },
- };
-
- assert.equal(await archHint(failing), null);
-});
-
-test('only the build a Mac hint names becomes primary', () => {
- assert.equal(buildVariant('x86_64', 'mac', 'x86_64'), 'primary');
- assert.equal(buildVariant('arm64', 'mac', 'x86_64'), 'secondary');
- assert.equal(buildVariant('arm64', 'mac', 'arm64'), 'primary');
- assert.equal(buildVariant('x86_64', 'mac', 'arm64'), 'secondary');
-});
-
-test('with no hint, both builds stay equal: Safari on an Intel Mac is not pointed at Apple silicon', () => {
- assert.equal(buildVariant('arm64', 'mac', null), 'secondary');
- assert.equal(buildVariant('x86_64', 'mac', null), 'secondary');
-});
-
-test('on an iPhone, an iPad or another system, neither Mac build is primary', () => {
- for (const device of ['ios', 'other'] as const) {
- assert.equal(buildVariant('arm64', device, 'arm64'), 'secondary');
- assert.equal(buildVariant('x86_64', device, 'x86_64'), 'secondary');
- assert.equal(buildVariant('arm64', device, null), 'secondary');
- }
-});
-
test('the App Store card leads only on an iPhone or iPad', () => {
assert.equal(appStoreFirst('ios'), true);
assert.equal(appStoreFirst('mac'), false);
assert.equal(appStoreFirst('other'), false);
});
-test('with no hint on a Mac, "Which Mac do I have?" opens; a pending or answered hint leaves it closed', () => {
- assert.equal(chipHelpOpen('mac', null), true);
- assert.equal(chipHelpOpen('mac', undefined), false);
- assert.equal(chipHelpOpen('mac', 'arm64'), false);
- assert.equal(chipHelpOpen('mac', 'x86_64'), false);
- assert.equal(chipHelpOpen('ios', null), false);
- assert.equal(chipHelpOpen('other', null), false);
- assert.equal(chipHelpOpen(null, null), false);
-});
-
/*
* The root template marks an iPhone or iPad with an `ios` class before first
* paint, which puts the App Store badge first and keeps the license banner off