🎉 初始提交:TinyLabel 图像文字编辑桌面工具

- 基于 Electron + React + TypeScript + Vite
- Fabric.js 画布编辑引擎
- Ant Design UI 组件 + Zustand 状态管理
- 支持文字添加、拖拽、字体/颜色/特效编辑
- 支持 PNG / JPEG / WebP 导出
This commit is contained in:
2026-07-15 11:49:12 +08:00
commit 08cfcb50b0
26 changed files with 9057 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron'
import { join, dirname } from 'path'
import { readFileSync, readdirSync } from 'fs'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
let mainWindow: BrowserWindow | null = null
// WSL/Linux 兼容配置:仅禁用沙箱 + X11 模式,GPU 让 Electron 自动处理
if (process.platform === 'linux') {
app.commandLine.appendSwitch('no-sandbox')
app.commandLine.appendSwitch('ozone-platform', 'x11')
app.commandLine.appendSwitch('in-process-gpu')
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
title: 'TinyLabel',
show: true,
center: true,
autoHideMenuBar: true,
backgroundColor: '#1a1a2e',
webPreferences: {
preload: join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
backgroundThrottling: false,
},
})
// 加载完成后最大化
mainWindow.once('ready-to-show', () => {
console.log('[main] ready-to-show')
clearTimeout(forceShowTimer)
mainWindow?.maximize()
mainWindow?.focus()
})
// 兜底:5 秒后强制弹窗
const forceShowTimer = setTimeout(() => {
if (mainWindow && !mainWindow.isVisible()) {
console.warn('[main] force-show window after timeout')
mainWindow.show()
mainWindow.focus()
}
}, 5000)
mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDesc) => {
console.error(`[main] load failed: ${errorCode} ${errorDesc}`)
})
const devUrl = process.env.VITE_DEV_SERVER_URL
if (devUrl) {
console.log(`[main] loading dev URL: ${devUrl}`)
mainWindow.loadURL(devUrl)
} else {
const filePath = join(__dirname, '../dist/index.html')
console.log(`[main] loading file: ${filePath}`)
mainWindow.loadFile(filePath)
}
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})
// 开发模式下打开 DevTools(内嵌在窗口底部)
if (devUrl) {
mainWindow.webContents.openDevTools()
}
}
app.whenReady().then(() => {
console.log('[main] app ready')
createWindow()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
// ─── IPC Handlers ────────────────────────────────────────────
ipcMain.handle('fs:readImages', async (_event, dirPath: string) => {
try {
const files = readdirSync(dirPath)
const images = files
.filter((f) => /\.(jpg|jpeg|png|webp|bmp)$/i.test(f))
.map((f) => {
const fullPath = join(dirPath, f)
const buffer = readFileSync(fullPath)
const ext = f.split('.').pop()!.toLowerCase()
const mime = ext === 'jpg' ? 'jpeg' : ext
return { name: f, data: `data:image/${mime};base64,${buffer.toString('base64')}` }
})
return images
} catch {
return []
}
})
ipcMain.handle('dialog:selectDirectory', async () => {
if (!mainWindow) return ''
const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'] })
return result.canceled ? '' : result.filePaths[0]
})
ipcMain.handle('dialog:saveFile', async (_event, defaultName: string) => {
if (!mainWindow) return ''
const result = await dialog.showSaveDialog(mainWindow, {
defaultPath: defaultName,
filters: [
{ name: 'Image', extensions: ['png', 'jpg', 'jpeg', 'webp'] },
],
})
return result.canceled ? '' : result.filePath
})
ipcMain.handle('fs:saveFile', async (_event, filePath: string, base64Data: string) => {
try {
const { writeFileSync } = await import('fs')
const buffer = Buffer.from(base64Data.split(',')[1], 'base64')
writeFileSync(filePath, buffer)
return true
} catch {
return false
}
})
ipcMain.handle('system:getFonts', async () => {
return [
'Arial', 'Arial Black', 'Calibri', 'Cambria', 'Comic Sans MS',
'Courier New', 'Georgia', 'Helvetica', 'Impact',
'Microsoft YaHei', 'Microsoft JhengHei',
'Noto Sans SC', 'Noto Serif SC', 'PingFang SC',
'Segoe UI', 'SimHei', 'SimSun',
'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana',
'Source Han Sans', 'Source Han Serif',
]
})
+13
View File
@@ -0,0 +1,13 @@
import { contextBridge, ipcRenderer } from 'electron'
const api = {
readImages: (dirPath: string) => ipcRenderer.invoke('fs:readImages', dirPath),
selectDirectory: () => ipcRenderer.invoke('dialog:selectDirectory'),
saveFile: (filePath: string, base64Data: string) =>
ipcRenderer.invoke('fs:saveFile', filePath, base64Data),
selectSavePath: (defaultName: string) =>
ipcRenderer.invoke('dialog:saveFile', defaultName),
getFonts: () => ipcRenderer.invoke('system:getFonts'),
}
contextBridge.exposeInMainWorld('electronAPI', api)