Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/build-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Build Desktop App

on:
push:
branches:
- main
tags:
- "v*"
paths:
- ".github/workflows/build-app.yml"
- "electron.vite.config.ts"
- "package.json"
- "package-lock.json"
- "tsconfig.json"
- "docs/icon.png"
- "src/**"
pull_request:
paths:
- ".github/workflows/build-app.yml"
- "electron.vite.config.ts"
- "package.json"
- "package-lock.json"
- "tsconfig.json"
- "docs/icon.png"
- "src/**"
workflow_dispatch:

permissions:
contents: read

jobs:
package:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: Windows x64
os: windows-2025
command: npm run dist:win
artifact: topspeed-builder-windows-x64
- name: macOS arm64
os: macos-15
command: npm run dist:mac:arm64
artifact: topspeed-builder-macos-arm64
- name: macOS Intel
os: macos-15-intel
command: npm run dist:mac:x64
artifact: topspeed-builder-macos-x64

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Package app
run: ${{ matrix.command }}
env:
CSC_IDENTITY_AUTO_DISCOVERY: false

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: release/**/*
if-no-files-found: error
retention-days: 14
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,35 @@ npm run dev # 开发模式
npm run typecheck # 类型检查
npm run build # 构建到 out/
npm run dist:win # 打包 Windows 安装包(输出到 release/)
npm run dist:mac:arm64 # 在 Apple Silicon Mac 上打包 DMG/ZIP
npm run dist:mac:x64 # 在 Intel Mac 上打包 DMG/ZIP
```

一键安装包位于 `release/Topspeed Builder Setup 1.0.3.exe`。
macOS 内测包位于 `release/Topspeed Builder-1.0.3-mac-arm64.dmg` 或 `release/Topspeed Builder-1.0.3-mac-x64.dmg`。

macOS 打包需要在 macOS 环境执行。CI 会通过 `.github/workflows/build-app.yml` 产出 Windows、macOS arm64 和 macOS x64 内测包;正式分发前需要 Apple Developer ID 签名和公证,流程见 [macOS Build and Release](./docs/mac-release.md)。

### macOS 命令行运行

如果暂时没有 macOS 安装包,Mac 用户可以从源码启动:

```bash
git clone https://github.com/voicepeak/topspeed-builder.git
cd topspeed-builder
git checkout macos
npm ci
npm run dev
```

也可以使用生产构建预览:

```bash
npm run build
npm start
```

建议使用 Node.js 22。若安装原生依赖失败,先执行 `xcode-select --install` 安装 Xcode Command Line Tools。

## 智能生成接口配置

Expand Down Expand Up @@ -181,9 +207,35 @@ npm run dev # dev mode
npm run typecheck # type checking
npm run build # compile to out/
npm run dist:win # package Windows installer (output in release/)
npm run dist:mac:arm64 # package Apple Silicon DMG/ZIP on macOS
npm run dist:mac:x64 # package Intel Mac DMG/ZIP on macOS
```

One-click installer at `release/Topspeed Builder Setup 1.0.3.exe`.
macOS test artifacts are written to `release/Topspeed Builder-1.0.3-mac-arm64.dmg` or `release/Topspeed Builder-1.0.3-mac-x64.dmg`.

macOS packaging must run on macOS. The CI workflow at `.github/workflows/build-app.yml` builds Windows, macOS arm64, and macOS x64 test artifacts. Public macOS distribution requires Apple Developer ID signing and notarization; see [macOS Build and Release](./docs/mac-release.md).

### macOS CLI Run

If a macOS installer is not available yet, Mac users can run the app from source:

```bash
git clone https://github.com/voicepeak/topspeed-builder.git
cd topspeed-builder
git checkout macos
npm ci
npm run dev
```

Production preview is also available:

```bash
npm run build
npm start
```

Node.js 22 is recommended. If native dependency installation fails, install Xcode Command Line Tools with `xcode-select --install`.

## AI API Configuration

Expand Down
61 changes: 61 additions & 0 deletions docs/mac-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# macOS Build and Release

This project supports macOS builds through electron-builder. Build macOS artifacts on macOS runners because the app uses `sharp`, a native dependency with platform-specific binaries.

## Internal Test Builds

Apple Silicon:

```bash
npm ci
npm run dist:mac:arm64
```

Intel Mac:

```bash
npm ci
npm run dist:mac:x64
```

Artifacts are written to `release/`:

- `Topspeed Builder-<version>-mac-arm64.dmg`
- `Topspeed Builder-<version>-mac-arm64.zip`
- `Topspeed Builder-<version>-mac-x64.dmg`
- `Topspeed Builder-<version>-mac-x64.zip`

The GitHub Actions workflow at `.github/workflows/build-app.yml` produces unsigned Windows and macOS artifacts for CI validation and internal testing.

## Smoke Test Checklist

Run this checklist on both Apple Silicon and Intel builds before publishing:

1. Launch the app from the DMG.
2. Create a project under `Documents/Topspeed Builder Projects`.
3. Import PNG/JPG/WebP reference images.
4. Switch provider to `local-draft` and generate an icon, a character sheet, and a tileset.
5. Use "show in folder" from an asset card.
6. Export with ZIP enabled and open the export directory.
7. Reopen the app and confirm the recent project list loads.

## Signed Distribution

Unsigned artifacts are suitable only for internal testing. Public macOS distribution should be signed with a Developer ID Application certificate and notarized by Apple.

Required GitHub Actions secrets for signed builds:

- `CSC_LINK`: Base64-encoded `.p12` certificate or a secure URL to it.
- `CSC_KEY_PASSWORD`: Password for the `.p12` certificate.
- `APPLE_ID`: Apple Developer account email.
- `APPLE_APP_SPECIFIC_PASSWORD`: App-specific password for notarization.
- `APPLE_TEAM_ID`: Apple Developer Team ID.

Signed scripts:

```bash
npm run dist:mac:signed:arm64
npm run dist:mac:signed:x64
```

After secrets are configured, run the signed scripts on macOS release runners and publish the notarized DMG/ZIP files.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
"dist": "npm run build && electron-builder",
"dist:win": "npm run build && electron-builder --win",
"dist:mac": "npm run build && electron-builder --mac",
"dist:mac:arm64": "npm run build && electron-builder --mac --arm64",
"dist:mac:x64": "npm run build && electron-builder --mac --x64",
"dist:mac:signed:arm64": "npm run build && electron-builder --mac --arm64 -c.mac.notarize=true",
"dist:mac:signed:x64": "npm run build && electron-builder --mac --x64 -c.mac.notarize=true",
"dist:linux": "npm run build && electron-builder --linux",
"typecheck": "tsc --noEmit"
},
Expand Down Expand Up @@ -48,7 +52,9 @@
},
"mac": {
"target": ["dmg", "zip"],
"icon": "docs/icon.png"
"icon": "docs/icon.png",
"category": "public.app-category.graphics-design",
"artifactName": "${productName}-${version}-mac-${arch}.${ext}"
},
"linux": {
"target": ["AppImage", "deb"],
Expand Down
5 changes: 5 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ function registerIpc(): void {
return true;
});

handle("shell:showProjectItem", async (projectPath: string, filePath: string) => {
shell.showItemInFolder(resolveProjectPath(projectPath, filePath));
return true;
});

handle("shell:openPath", async (filePath: string) => {
const result = await shell.openPath(filePath);
if (result) {
Expand Down
2 changes: 2 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@ contextBridge.exposeInMainWorld("topspeedBuilder", {
ipcRenderer.invoke("image:dataUrl", projectPath, filePath),
deleteAsset: (projectPath: string, assetId: string) => ipcRenderer.invoke("project:deleteAsset", projectPath, assetId),
showItemInFolder: (filePath: string) => ipcRenderer.invoke("shell:showItem", filePath),
showProjectItemInFolder: (projectPath: string, filePath: string) =>
ipcRenderer.invoke("shell:showProjectItem", projectPath, filePath),
openPath: (filePath: string) => ipcRenderer.invoke("shell:openPath", filePath)
});
9 changes: 1 addition & 8 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1586,7 +1586,7 @@ function AssetCard(props: {
onClick={() => {
const file = props.asset.metadataPath ?? pngFile;
if (file) {
void props.runTask(t("busy.locateFile"), () => unwrap(window.topspeedBuilder.showItemInFolder(resolveFile(props.project.path, file))));
void props.runTask(t("busy.locateFile"), () => unwrap(window.topspeedBuilder.showProjectItemInFolder(props.project.path, file)));
}
}}
>
Expand Down Expand Up @@ -1874,13 +1874,6 @@ function gameTypeLabelT(t: (key: string) => string, gameType: string): string {
return found ? t(selectOptionLabel(found)) : gameType;
}

function resolveFile(projectPath: string, filePath: string): string {
if (/^[a-zA-Z]:[\\/]/.test(filePath) || filePath.startsWith("/")) {
return filePath;
}
return `${projectPath}\\${filePath.replace(/\//g, "\\")}`;
}

function formatQueueError(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
const normalized = message.replace(/\s+/g, " ").trim();
Expand Down
1 change: 1 addition & 0 deletions src/renderer/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ declare global {
getHistory(projectPath: string): Promise<IpcResponse<GenerationHistoryRecord[]>>;
readImageDataUrl(projectPath: string, filePath: string): Promise<IpcResponse<string>>;
showItemInFolder(filePath: string): Promise<IpcResponse<boolean>>;
showProjectItemInFolder(projectPath: string, filePath: string): Promise<IpcResponse<boolean>>;
openPath(filePath: string): Promise<IpcResponse<boolean>>;
};
}
Expand Down
Loading