feat: add Electron desktop client with Windows installer support
This commit is contained in:
1 parent
3fb7e90b05
commit
3405a24531
11 files changed
+4337
-5
No files matched your search
@@ -39,3 +39,6 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# electron
|
||||||
|
/dist
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||||
|
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||||
@@ -66,6 +66,18 @@ I watched the versatility of these models being gutted. I don't want us to live
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 💻 Desktop Client
|
||||||
|
|
||||||
|
**No Node.js required!** Download the pre-built installer:
|
||||||
|
|
||||||
|
- 📥 [Download Author Setup (Windows)](https://github.com/YuanShiJiLoong/author/releases/latest)
|
||||||
|
|
||||||
|
Just install and start writing. All features work out of the box.
|
||||||
|
|
||||||
|
> 💡 To build the desktop app from source: `npm run build && npx electron-builder --win`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|||||||
@@ -66,6 +66,19 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 💻 桌面客户端
|
||||||
|
|
||||||
|
**无需安装 Node.js!** 直接下载安装包:
|
||||||
|
|
||||||
|
- 📥 [下载 Author 安装包(Windows)](https://github.com/YuanShiJiLoong/author/releases/latest)
|
||||||
|
- 📥 [国内下载(Gitee)](https://gitee.com/yuanshijilong/author/releases)
|
||||||
|
|
||||||
|
安装即用,所有功能开箱即得。
|
||||||
|
|
||||||
|
> 💡 从源码构建桌面应用:`npm run build && npx electron-builder --win`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
const { app, BrowserWindow, shell } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
let mainWindow;
|
||||||
|
let nextServer;
|
||||||
|
|
||||||
|
const isDev = process.argv.includes('--dev');
|
||||||
|
const PORT = 3000;
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1400,
|
||||||
|
height: 900,
|
||||||
|
minWidth: 900,
|
||||||
|
minHeight: 600,
|
||||||
|
title: 'Author — AI-Powered Creative Writing',
|
||||||
|
icon: path.join(__dirname, '..', 'public', 'icon.png'),
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
},
|
||||||
|
// 隐藏默认菜单栏,更简洁
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.loadURL(`http://localhost:${PORT}`);
|
||||||
|
|
||||||
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 外部链接在系统浏览器中打开
|
||||||
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
if (url.startsWith('http')) {
|
||||||
|
shell.openExternal(url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
}
|
||||||
|
return { action: 'allow' };
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startNextServer() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (isDev) {
|
||||||
|
// 开发模式:假设 dev server 已在运行
|
||||||
|
console.log('[Author] Dev mode — connecting to existing dev server...');
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Author] Starting production server...');
|
||||||
|
|
||||||
|
// 生产模式:运行 next start
|
||||||
|
const nextBin = path.join(__dirname, '..', 'node_modules', '.bin', 'next');
|
||||||
|
const projectDir = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
nextServer = spawn(
|
||||||
|
process.platform === 'win32' ? `${nextBin}.cmd` : nextBin,
|
||||||
|
['start', '-p', String(PORT)],
|
||||||
|
{
|
||||||
|
cwd: projectDir,
|
||||||
|
env: { ...process.env, NODE_ENV: 'production' },
|
||||||
|
stdio: 'pipe',
|
||||||
|
shell: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
nextServer.stdout.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
console.log('[Next.js]', output);
|
||||||
|
if (!started && output.includes('Ready')) {
|
||||||
|
started = true;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nextServer.stderr.on('data', (data) => {
|
||||||
|
console.error('[Next.js Error]', data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
nextServer.on('error', (err) => {
|
||||||
|
console.error('[Next.js] Failed to start:', err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
nextServer.on('close', (code) => {
|
||||||
|
if (!started) {
|
||||||
|
reject(new Error(`Next.js server exited with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 超时保底 — 15秒内没有就绪也继续
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
console.log('[Author] Server startup timeout, loading anyway...');
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
try {
|
||||||
|
await startNextServer();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Author] Server start failed:', err);
|
||||||
|
}
|
||||||
|
createWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (nextServer) {
|
||||||
|
nextServer.kill();
|
||||||
|
}
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确保退出时清理子进程
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
if (nextServer) {
|
||||||
|
nextServer.kill();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const { contextBridge } = require('electron');
|
||||||
|
|
||||||
|
// 最小预加载脚本 — 安全隔离
|
||||||
|
// 如果将来需要从渲染进程调用 Node.js API,可以通过 contextBridge 暴露
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
isElectron: true,
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
devIndicators: false,
|
devIndicators: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Generated
+4119
-2
File diff suppressed because it is too large.
Load diff
+41
-3
@@ -1,12 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "author-app",
|
"name": "author-app",
|
||||||
"version": "0.1.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"main": "electron/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint",
|
||||||
|
"electron:dev": "npm run dev & npx electron . --dev",
|
||||||
|
"electron:build": "next build && npx electron-builder --win",
|
||||||
|
"electron:start": "npx electron ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/extension-character-count": "^3.19.0",
|
"@tiptap/extension-character-count": "^3.19.0",
|
||||||
@@ -38,9 +42,43 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"electron": "^35.7.5",
|
||||||
|
"electron-builder": "^26.8.1",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"pdf-parse": "^2.4.5",
|
"pdf-parse": "^2.4.5",
|
||||||
"tailwindcss": "^4"
|
"tailwindcss": "^4"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.yuanshijilong.author",
|
||||||
|
"productName": "Author",
|
||||||
|
"directories": {
|
||||||
|
"output": "dist"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".next/**/*",
|
||||||
|
"node_modules/**/*",
|
||||||
|
"public/**/*",
|
||||||
|
"electron/**/*",
|
||||||
|
"package.json",
|
||||||
|
"next.config.mjs"
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "nsis",
|
||||||
|
"arch": [
|
||||||
|
"x64"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"perMachine": false,
|
||||||
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Reference in new issue
Block a user