You've already forked Nano-Banana-AI-Image-Editor
新增 现在支持编译为Windows桌面端
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,3 +23,5 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
release
|
||||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||||
|
ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||||
54
build/README.md
Normal file
54
build/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# 应用图标
|
||||||
|
|
||||||
|
此目录包含用于打包桌面应用程序的图标文件。
|
||||||
|
|
||||||
|
## Windows 平台
|
||||||
|
|
||||||
|
Windows 平台需要 ICO 格式的图标文件,命名为 `icon.ico`。
|
||||||
|
|
||||||
|
### 如何创建 ICO 文件
|
||||||
|
|
||||||
|
有几种方法可以创建 ICO 文件:
|
||||||
|
|
||||||
|
#### 方法1:使用 Python 脚本(推荐)
|
||||||
|
|
||||||
|
项目根目录下提供了一个 Python 脚本 `convert_svg_to_ico.py`,可以直接将 SVG 转换为 ICO 格式:
|
||||||
|
|
||||||
|
1. 确保已安装 Python 3 和必要的依赖包:
|
||||||
|
```
|
||||||
|
pip install Pillow cairosvg
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 在 Windows 上,您可能还需要安装 GTK3 运行时环境:
|
||||||
|
- 下载并安装 GTK3: https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer
|
||||||
|
|
||||||
|
3. 运行转换脚本:
|
||||||
|
```
|
||||||
|
npm run electron:icon
|
||||||
|
```
|
||||||
|
或直接运行:
|
||||||
|
```
|
||||||
|
python convert_svg_to_ico.py
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会自动将 `public/favicon.svg` 转换为 `build/icon.ico`,并包含多个尺寸。
|
||||||
|
|
||||||
|
#### 方法2:使用在线转换工具
|
||||||
|
|
||||||
|
- [ConvertICO](https://convertico.com/)
|
||||||
|
- [Online-Convert](https://image.online-convert.com/convert-to-ico)
|
||||||
|
- [CloudConvert](https://cloudconvert.com/png-to-ico)
|
||||||
|
|
||||||
|
#### 方法3:使用图像编辑软件
|
||||||
|
|
||||||
|
使用支持 ICO 格式的图像编辑软件(如 GIMP、Photoshop 插件等)手动创建。
|
||||||
|
|
||||||
|
### 图标尺寸要求
|
||||||
|
|
||||||
|
Windows 应用程序图标应包含多个尺寸:
|
||||||
|
- 16x16
|
||||||
|
- 32x32
|
||||||
|
- 48x48
|
||||||
|
- 256x256
|
||||||
|
|
||||||
|
确保在生成 ICO 文件时包含这些尺寸。
|
||||||
BIN
build/icon.ico
Normal file
BIN
build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
14
copy-favicon.js
Normal file
14
copy-favicon.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { copyFileSync, existsSync, mkdirSync } from 'fs';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
const source = resolve('./public/favicon.svg');
|
||||||
|
const dest = resolve('./dist/favicon.svg');
|
||||||
|
|
||||||
|
// 确保 dist 目录存在
|
||||||
|
if (!existsSync('./dist')) {
|
||||||
|
mkdirSync('./dist');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制文件
|
||||||
|
copyFileSync(source, dest);
|
||||||
|
console.log('favicon.svg copied successfully');
|
||||||
56
electron/dev-runner.js
Normal file
56
electron/dev-runner.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
const { spawn } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// 先编译 TypeScript 文件
|
||||||
|
const tsc = spawn('npx', ['tsc', '-p', 'tsconfig.electron.json'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
tsc.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
// 编译成功后复制 preload.js 文件
|
||||||
|
const sourcePreload = path.join(__dirname, 'preload.js');
|
||||||
|
const destPreload = path.join(__dirname, 'dist', 'preload.js');
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(sourcePreload, destPreload);
|
||||||
|
console.log('preload.js copied successfully');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy preload.js:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译成功后运行 Electron 应用
|
||||||
|
const electron = require('electron');
|
||||||
|
const appPath = path.join(__dirname, 'dist', 'main.js');
|
||||||
|
|
||||||
|
// 启动 Vite 开发服务器
|
||||||
|
const vite = spawn('npm', ['run', 'dev'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: true,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
VITE_DEV_SERVER_URL: 'http://localhost:5173',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待一段时间让 Vite 服务器启动,然后启动 Electron
|
||||||
|
setTimeout(() => {
|
||||||
|
const child = spawn(electron, [appPath], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
VITE_DEV_SERVER_URL: 'http://localhost:5173',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
process.exit(code || 0);
|
||||||
|
});
|
||||||
|
}, 5000); // 等待 5 秒钟让 Vite 服务器启动
|
||||||
|
} else {
|
||||||
|
console.error('TypeScript compilation failed');
|
||||||
|
process.exit(code || 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
3
electron/index.js
Normal file
3
electron/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Electron 入口文件
|
||||||
|
// 使用 require 而不是 import 来避免 ES 模块问题
|
||||||
|
require('./dist/main.js');
|
||||||
59
electron/main.js
Normal file
59
electron/main.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { app, BrowserWindow } from 'electron';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||||
|
if (require('electron-squirrel-startup')) {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const createWindow = () => {
|
||||||
|
// Create the browser window.
|
||||||
|
const mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// and load the index.html of the app.
|
||||||
|
if (process.env.VITE_DEV_SERVER_URL) {
|
||||||
|
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the DevTools.
|
||||||
|
if (process.env.VITE_DEV_SERVER_URL) {
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// This method will be called when Electron has finished
|
||||||
|
// initialization and is ready to create browser windows.
|
||||||
|
// Some APIs can only be used after this event occurs.
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
// On OS X it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
|
// for applications and their menu bar to stay active until the user quits
|
||||||
|
// explicitly with Cmd + Q.
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// In this file you can include the rest of your app's specific main process
|
||||||
|
// code. You can also put them in separate files and import them here.
|
||||||
93
electron/main.ts
Normal file
93
electron/main.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||||
|
if (require('electron-squirrel-startup')) {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
const createWindow = () => {
|
||||||
|
// Create the browser window.
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
frame: false, // 隐藏默认的窗口框架
|
||||||
|
icon: path.join(__dirname, '../../build/icon.ico'), // 设置应用图标
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// and load the index.html of the app.
|
||||||
|
if (process.env.VITE_DEV_SERVER_URL) {
|
||||||
|
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||||
|
} else {
|
||||||
|
// 使用绝对路径从项目根目录的 dist 文件夹加载
|
||||||
|
const indexPath = path.join(__dirname, '../../dist/index.html');
|
||||||
|
console.log('Loading index.html from:', indexPath);
|
||||||
|
mainWindow.loadFile(indexPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the DevTools.
|
||||||
|
if (process.env.VITE_DEV_SERVER_URL) {
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// IPC 处理程序
|
||||||
|
ipcMain.handle('window-minimize', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.minimize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('window-maximize', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMaximized()) {
|
||||||
|
mainWindow.unmaximize();
|
||||||
|
} else {
|
||||||
|
mainWindow.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('window-close', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('window-is-maximized', () => {
|
||||||
|
return mainWindow ? mainWindow.isMaximized() : false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// This method will be called when Electron has finished
|
||||||
|
// initialization and is ready to create browser windows.
|
||||||
|
// Some APIs can only be used after this event occurs.
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
// On OS X it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
|
// for applications and their menu bar to stay active until the user quits
|
||||||
|
// explicitly with Cmd + Q.
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// In this file you can include the rest of your app's specific main process
|
||||||
|
// code. You can also put them in separate files and import them here.
|
||||||
3
electron/package.json
Normal file
3
electron/package.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
20
electron/preload.js
Normal file
20
electron/preload.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const { contextBridge, ipcRenderer, remote } = require('electron');
|
||||||
|
|
||||||
|
// As an example, we expose a function to the renderer process
|
||||||
|
// that shows a dialog
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
showDialog: () => ipcRenderer.invoke('show-dialog'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom APIs for renderer
|
||||||
|
const api = {};
|
||||||
|
|
||||||
|
// 窗口控制 API
|
||||||
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
|
minimize: () => ipcRenderer.invoke('window-minimize'),
|
||||||
|
maximize: () => ipcRenderer.invoke('window-maximize'),
|
||||||
|
close: () => ipcRenderer.invoke('window-close'),
|
||||||
|
isMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = api;
|
||||||
52
electron/prod-runner.js
Normal file
52
electron/prod-runner.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
const { spawn } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// 先编译 TypeScript 文件
|
||||||
|
const tsc = spawn('npx', ['tsc', '-p', 'tsconfig.electron.json'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
tsc.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
// 编译成功后复制 preload.js 文件
|
||||||
|
const sourcePreload = path.join(__dirname, 'preload.js');
|
||||||
|
const destPreload = path.join(__dirname, 'dist', 'preload.js');
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(sourcePreload, destPreload);
|
||||||
|
console.log('preload.js copied successfully');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy preload.js:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译成功后运行 Electron 应用
|
||||||
|
const electron = require('electron');
|
||||||
|
const appPath = path.join(__dirname, 'dist', 'main.js');
|
||||||
|
|
||||||
|
// 先确保应用已构建
|
||||||
|
const build = spawn('npm', ['run', 'build'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
build.on('close', (buildCode) => {
|
||||||
|
if (buildCode === 0) {
|
||||||
|
const child = spawn(electron, [appPath], {
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
process.exit(code || 0);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Build failed');
|
||||||
|
process.exit(buildCode || 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('TypeScript compilation failed');
|
||||||
|
process.exit(code || 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍌</text></svg>" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Nano Banana AI 图像编辑器 - AI 图像生成器和编辑器</title>
|
<title>Nano Banana AI 图像编辑器 - AI 图像生成器和编辑器</title>
|
||||||
<meta name="description" content="由 Gemini 2.5 Flash Image 提供支持的专业 AI 图像生成和对话式编辑。使用自然语言提示创建、编辑和增强图像。" />
|
<meta name="description" content="由 Gemini 2.5 Flash Image 提供支持的专业 AI 图像生成和对话式编辑。使用自然语言提示创建、编辑和增强图像。" />
|
||||||
|
|||||||
8833
package-lock.json
generated
8833
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -8,13 +8,24 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.pandorastudio.cn/yuantao/Nano-Banana-AI-Image-Editor.git"
|
"url": "https://git.pandorastudio.cn/yuantao/Nano-Banana-AI-Image-Editor.git"
|
||||||
},
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "Mark Fulton",
|
||||||
|
"email": "markfulton@example.com",
|
||||||
|
"url": "https://markfulton.com"
|
||||||
|
},
|
||||||
|
"main": "electron/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build && node copy-favicon.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch"
|
"test:watch": "jest --watch",
|
||||||
|
"electron:dev": "node electron/dev-runner.js",
|
||||||
|
"electron:build": "vite build && copy public\\favicon.svg dist\\ && electron-builder --win --x64",
|
||||||
|
"electron:preview": "node electron/prod-runner.js",
|
||||||
|
"electron:compile": "tsc -p tsconfig.electron.json",
|
||||||
|
"postinstall": "echo Skipping electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^1.16.0",
|
"@google/genai": "^1.16.0",
|
||||||
@@ -25,6 +36,8 @@
|
|||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"fabric": "^6.7.1",
|
"fabric": "^6.7.1",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"konva": "^9.3.22",
|
"konva": "^9.3.22",
|
||||||
@@ -46,6 +59,8 @@
|
|||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.18",
|
||||||
|
"electron": "^38.2.1",
|
||||||
|
"electron-builder": "^26.0.12",
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.9.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.11",
|
"eslint-plugin-react-refresh": "^0.4.11",
|
||||||
@@ -53,10 +68,38 @@
|
|||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest-environment-jsdom": "^30.1.2",
|
"jest-environment-jsdom": "^30.1.2",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
|
"pump": "^3.0.3",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"tar": "^7.4.3",
|
||||||
"ts-jest": "^29.4.3",
|
"ts-jest": "^29.4.3",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.3.0",
|
"typescript-eslint": "^8.3.0",
|
||||||
"vite": "^5.4.2"
|
"vite": "^5.4.2"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.nanobanana.editor",
|
||||||
|
"productName": "Nano Banana AI Image Editor",
|
||||||
|
"npmRebuild": false,
|
||||||
|
"electronDownload": {
|
||||||
|
"mirror": "https://npmmirror.com/mirrors/electron/",
|
||||||
|
"customDir": "38.2.1",
|
||||||
|
"customFilename": "electron-v38.2.1-win32-x64.zip"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"output": "release",
|
||||||
|
"buildResources": "build"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"electron/**/*",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
"nsis",
|
||||||
|
"zip"
|
||||||
|
],
|
||||||
|
"icon": "build/icon.ico"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
public/favicon.svg
Normal file
4
public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<circle cx="50" cy="50" r="45" fill="#FDE047"/>
|
||||||
|
<text x="50" y="70" font-size="60" text-anchor="middle" fill="#000">🍌</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 201 B |
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { cn } from './utils/cn';
|
import { cn } from './utils/cn';
|
||||||
import { Header } from './components/Header';
|
import { CustomTitleBar } from './components/CustomTitleBar';
|
||||||
import { PromptComposer } from './components/PromptComposer';
|
import { PromptComposer } from './components/PromptComposer';
|
||||||
import { ImageCanvas } from './components/ImageCanvas';
|
import { ImageCanvas } from './components/ImageCanvas';
|
||||||
import { HistoryPanel } from './components/HistoryPanel';
|
import { HistoryPanel } from './components/HistoryPanel';
|
||||||
@@ -91,9 +91,7 @@ function AppContent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-gray-50 text-gray-900 flex flex-col font-sans">
|
<div className="h-screen bg-gray-50 text-gray-900 flex flex-col font-sans">
|
||||||
<div className="card card-lg rounded-none">
|
<CustomTitleBar />
|
||||||
<Header />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 flex overflow-hidden p-4 gap-4 relative">
|
<div className="flex-1 flex overflow-hidden p-4 gap-4 relative">
|
||||||
<div className={cn("flex-shrink-0 transition-all duration-300 ease-in-out overflow-hidden", showPromptPanel ? "w-72" : "w-8")}>
|
<div className={cn("flex-shrink-0 transition-all duration-300 ease-in-out overflow-hidden", showPromptPanel ? "w-72" : "w-8")}>
|
||||||
|
|||||||
107
src/components/CustomTitleBar.tsx
Normal file
107
src/components/CustomTitleBar.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from './ui/Button';
|
||||||
|
import { HelpCircle, Minus, Square, X } from 'lucide-react';
|
||||||
|
import { InfoModal } from './InfoModal';
|
||||||
|
|
||||||
|
export const CustomTitleBar: React.FC = () => {
|
||||||
|
const [showInfoModal, setShowInfoModal] = useState(false);
|
||||||
|
const [isMaximized, setIsMaximized] = useState(false);
|
||||||
|
|
||||||
|
// 检查窗口是否最大化
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMaximized = () => {
|
||||||
|
if (window.electron && window.electron.isMaximized) {
|
||||||
|
window.electron.isMaximized().then(setIsMaximized);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkMaximized();
|
||||||
|
|
||||||
|
// 监听窗口状态变化
|
||||||
|
const handleResize = () => {
|
||||||
|
checkMaximized();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const minimizeWindow = () => {
|
||||||
|
if (window.electron && window.electron.minimize) {
|
||||||
|
window.electron.minimize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const maximizeWindow = () => {
|
||||||
|
if (window.electron && window.electron.maximize) {
|
||||||
|
window.electron.maximize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeWindow = () => {
|
||||||
|
if (window.electron && window.electron.close) {
|
||||||
|
window.electron.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className="h-12 bg-white flex items-center justify-between px-3 rounded-t-xl border-b border-gray-200 draggable">
|
||||||
|
<div className="flex items-center space-x-2 non-draggable">
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-yellow-50">
|
||||||
|
{/* 使用应用的主要图标 */}
|
||||||
|
<img src="./favicon.svg" alt="App Icon" className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-base font-medium text-gray-800 hidden sm:block">
|
||||||
|
Nano Banana AI 图像编辑器
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-1 non-draggable">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setShowInfoModal(true)}
|
||||||
|
className="h-7 w-7 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full card non-draggable"
|
||||||
|
>
|
||||||
|
<HelpCircle className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 窗口控制按钮 - 仅在 Electron 环境中显示 */}
|
||||||
|
{window.electron && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={minimizeWindow}
|
||||||
|
className="h-7 w-7 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full card non-draggable"
|
||||||
|
>
|
||||||
|
<Minus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={maximizeWindow}
|
||||||
|
className="h-7 w-7 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full card non-draggable"
|
||||||
|
>
|
||||||
|
<Square className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={closeWindow}
|
||||||
|
className="h-7 w-7 text-gray-500 hover:text-gray-700 hover:bg-red-100 rounded-full card non-draggable"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<InfoModal open={showInfoModal} onOpenChange={setShowInfoModal} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -232,4 +232,53 @@ body {
|
|||||||
|
|
||||||
.card-lg {
|
.card-lg {
|
||||||
@apply shadow-card-lg;
|
@apply shadow-card-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Electron window drag regions */
|
||||||
|
.draggable {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.non-draggable {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 窗口控制按钮 */
|
||||||
|
.window-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 30px;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button {
|
||||||
|
width: 45px;
|
||||||
|
height: 30px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #000;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls .close:hover {
|
||||||
|
background: #ff5f56;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls .minimize:hover {
|
||||||
|
background: #ffbd2e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls .maximize:hover {
|
||||||
|
background: #27c93f;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
30
tsconfig.electron.json
Normal file
30
tsconfig.electron.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"ES2023"
|
||||||
|
],
|
||||||
|
"module": "commonjs",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "electron/dist",
|
||||||
|
"rootDir": "electron",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"typeRoots": [
|
||||||
|
"node_modules/@types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"electron/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: './', // 添加这行以确保资源路径正确
|
||||||
|
publicDir: path.resolve(__dirname, 'public'), // 使用绝对路径指定 public 目录
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: ['lucide-react'],
|
exclude: ['lucide-react'],
|
||||||
@@ -14,4 +21,20 @@ export default defineConfig({
|
|||||||
'react-day-picker/locale': 'date-fns/locale',
|
'react-day-picker/locale': 'date-fns/locale',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'assets',
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'assets/[name].[hash].js',
|
||||||
|
chunkFileNames: 'assets/[name].[hash].js',
|
||||||
|
assetFileNames: 'assets/[name].[hash].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 确保 public 目录下的文件被复制到 dist 目录
|
||||||
|
copyPublicDir: true,
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'import.meta.env.VITE_DEV_SERVER_URL': JSON.stringify(process.env.VITE_DEV_SERVER_URL),
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user