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
|
||||
*.tsbuildinfo
|
||||
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
|
||||
|
||||
### 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} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
devIndicators: false,
|
||||
};
|
||||
|
||||
|
||||
Generated
+4119
-2
File diff suppressed because it is too large.
Load diff
+41
-3
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"name": "author-app",
|
||||
"version": "0.1.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"main": "electron/main.js",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"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": {
|
||||
"@tiptap/extension-character-count": "^3.19.0",
|
||||
@@ -38,9 +42,43 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"electron": "^35.7.5",
|
||||
"electron-builder": "^26.8.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"pdf-parse": "^2.4.5",
|
||||
"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