commit 201619055d93f831e28383d161530493fd47d0d3 Author: mozzie Date: Thu Aug 29 16:59:25 2024 +0800 first commit diff --git a/apps/desktop/.eslintrc.cjs b/apps/desktop/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/apps/desktop/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/apps/desktop/README.md b/apps/desktop/README.md new file mode 100644 index 0000000..0d6babe --- /dev/null +++ b/apps/desktop/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/apps/desktop/components.json b/apps/desktop/components.json new file mode 100644 index 0000000..5fc2fd9 --- /dev/null +++ b/apps/desktop/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/style/global.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/apps/desktop/electron-builder.json5 b/apps/desktop/electron-builder.json5 new file mode 100644 index 0000000..d8e386b --- /dev/null +++ b/apps/desktop/electron-builder.json5 @@ -0,0 +1,43 @@ +// @see - https://www.electron.build/configuration/configuration +{ + "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", + "appId": "YourAppID", + "asar": true, + "productName": "YourAppName", + "directories": { + "output": "release/${version}" + }, + "files": [ + "dist", + "dist-electron" + ], + "mac": { + "target": [ + "dmg" + ], + "artifactName": "${productName}-Mac-${version}-Installer.${ext}" + }, + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + } + ], + "artifactName": "${productName}-Windows-${version}-Setup.${ext}" + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "deleteAppDataOnUninstall": false + }, + "linux": { + "target": [ + "AppImage" + ], + "artifactName": "${productName}-Linux-${version}.${ext}" + } +} diff --git a/apps/desktop/electron/core/PythonManager.ts b/apps/desktop/electron/core/PythonManager.ts new file mode 100644 index 0000000..4f096b1 --- /dev/null +++ b/apps/desktop/electron/core/PythonManager.ts @@ -0,0 +1,101 @@ +import http from "node:http"; +import path from "node:path"; +import { spawn, ChildProcess } from "node:child_process"; +import { BrowserWindow } from "electron"; + +class PythonManager { + public flaskProcess: ChildProcess | null = null; + private intervalId: NodeJS.Timeout | null = null; + + constructor( + private mainWindow: BrowserWindow | null, + private url: string, + private interval = 5000 + ) {} + + // 启动 Python 服务 + public startFlask() { + if (this.flaskProcess) { + console.log("Flask service is already running."); + this.mainWindow?.webContents.send("flask", { running: true }); + return; + } + + // 使用 spawn 启动 Flask 服务 + this.flaskProcess = spawn(path.join(process.env.VITE_PUBLIC!, "flask_app")); + + // 实时获取 stdout 日志 + this.flaskProcess.stdout?.on("data", (data) => { + const message = data.toString(); + console.log(`Flask stdout: ${message}`); + this.mainWindow?.webContents.send("flask", { stdout: message }); + }); + + // 实时获取 stderr 日志 + this.flaskProcess.stderr?.on("data", (data) => { + const message = data.toString(); + console.error(`Flask stderr: ${message}`); + this.mainWindow?.webContents.send("flask-service:response", { stderr: message }); + }); + + // 监听进程关闭事件 + this.flaskProcess.on("close", (code) => { + console.log(`Flask process exited with code ${code}`); + this.flaskProcess = null; + this.mainWindow?.webContents.send("flask-service:response", { exited: true, code }); + }); + + // 开始轮询服务状态 + // this.startCheckingFlaskStatus(); + } + + // 停止 Python 服务 + public stopFlask() { + if (this.flaskProcess) { + this.flaskProcess.kill(); + console.log("Flask service stopped."); + this.flaskProcess = null; + } + + // 停止轮询 + this.stopCheckingFlaskStatus(); + } + + // 检查 Flask 服务状态 + private checkFlaskStatus() { + if (!this.mainWindow) return; + + http + .get(this.url, (res) => { + const { statusCode } = res; + this.mainWindow?.webContents.send("flask-check", { + running: statusCode === 200, + }); + }) + .on("error", (err) => { + console.error(`Error checking Flask service: ${err.message}`); + this.mainWindow?.webContents.send("flask-check", { + running: false, + }); + }); + } + + // 开始轮询 Flask 服务状态 + private startCheckingFlaskStatus() { + if (this.intervalId) { + console.log("Already checking Flask status."); + return; + } + this.intervalId = setInterval(() => this.checkFlaskStatus(), this.interval); + } + + // 停止轮询 Flask 服务状态 + private stopCheckingFlaskStatus() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } +} + +export default PythonManager; diff --git a/apps/desktop/electron/core/db.ts b/apps/desktop/electron/core/db.ts new file mode 100644 index 0000000..4a5a758 --- /dev/null +++ b/apps/desktop/electron/core/db.ts @@ -0,0 +1,27 @@ +import path from "node:path"; +import { JSONFilePreset } from "lowdb/node"; +import { app } from "electron"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const initDb = async () => { + // Read or create db.json + const defaultData = { posts: [] }; + const db = await JSONFilePreset( + path.join(app.getPath("userData"), "db.json"), + defaultData + ); + + // Update db.json + await db.update(({ posts }) => posts.push("hello world")); + + // Alternatively you can call db.write() explicitely later + // to write to db.json + db.data.posts.push("hello world"); + await db.write(); + + console.log(db); +}; + +initDb(); diff --git a/apps/desktop/electron/core/dicom.ts b/apps/desktop/electron/core/dicom.ts new file mode 100644 index 0000000..d74f1a2 --- /dev/null +++ b/apps/desktop/electron/core/dicom.ts @@ -0,0 +1,123 @@ +import path from "path"; +import * as dicomParser from "dicom-parser"; +import fs from "fs"; + +export interface StructuredData { + [StudyInstanceUID: string]: { + [SeriesInstanceUID: string]: ExtractMetadata[]; + }; +} + +export interface ExtractMetadata { + filePath: string; + StudyInstanceUID?: string; + SeriesInstanceUID?: string; + pixelData?: Uint16Array; +} + +/** + * 定义一个异步函数来递归地查找.dcm文件 + * @param dir + * @param fileList + * @returns + */ +export const findDcmFiles = async ( + dir: string, + fileList: string[] = [] +): Promise => { + const files = await fs.promises.readdir(dir, { withFileTypes: true }); + await Promise.all( + files.map(async (file) => { + const filePath = path.join(dir, file.name); + if (file.isDirectory()) { + await findDcmFiles(filePath, fileList); // 递归调用以遍历子目录 + } else if (file.name.endsWith(".dcm")) { + fileList.push(filePath); // 如果文件是.dcm文件,添加到列表中 + } + }) + ); + return fileList; +}; + +/** + * 获取单个dcm文件的metadata信息 + */ +export const parseDICOMFile = async ( + filePath: string +): Promise => { + try { + const arrayBuffer = await fs.promises.readFile(filePath); + const byteArray = new Uint8Array(arrayBuffer); + const options = { TransferSyntaxUID: "1.2.840.10008.1.2" }; + const dataSet = dicomParser.parseDicom(byteArray, options); + const StudyInstanceUID = dataSet.string("x0020000d"); + const SeriesInstanceUID = dataSet.string("x0020000e"); + const pixelDataElement = dataSet.elements.x7fe00010; + const pixelData = new Uint16Array( + dataSet.byteArray.buffer, + pixelDataElement.dataOffset, + pixelDataElement.length / 2 + ); + + return { + filePath, + StudyInstanceUID, + SeriesInstanceUID, + // pixelData, + }; + } catch (error) { + console.error(`Error parsing file ${filePath}:`, error); + return undefined; + } +}; + +/** + * 处理文件的函数,分批异步处理 + * @param filePaths + * @param {number} batchSize 批次数 + * @returns + */ +export const processFilesInBatches = async ( + filePaths: string[], + batchSize: number +) => { + const results = []; + for (let i = 0; i < filePaths.length; i += batchSize) { + const batch = filePaths.slice(i, i + batchSize); + const batchResults = await Promise.allSettled( + batch.map((filePath) => parseDICOMFile(filePath)) + ); + // 只提取状态为 'fulfilled' 的结果的 value + const fulfilledResults = batchResults + .filter((result) => result.status === "fulfilled") + .map( + (result) => (result as PromiseFulfilledResult).value + ); + results.push(...fulfilledResults); + } + return results; +}; + +export const structureMetadata = (data: ExtractMetadata[]): StructuredData => { + const structured: StructuredData = {}; + + data.forEach((item) => { + // 确保每个元素都有有效的 StudyInstanceUID 和 SeriesInstanceUID + if (item.StudyInstanceUID && item.SeriesInstanceUID) { + // 如果还没有为这个 StudyInstanceUID 创建记录,则初始化一个空对象 + if (!structured[item.StudyInstanceUID]) { + structured[item.StudyInstanceUID] = {}; + } + + // 如果这个 StudyInstanceUID 下还没有这个 SeriesInstanceUID 的记录,则初始化一个空数组 + if (!structured[item.StudyInstanceUID][item.SeriesInstanceUID]) { + structured[item.StudyInstanceUID][item.SeriesInstanceUID] = []; + } + + // 将当前元素添加到对应的数组中 + structured[item.StudyInstanceUID][item.SeriesInstanceUID].push(item); + } + }); + + return structured; +}; diff --git a/apps/desktop/electron/electron-env.d.ts b/apps/desktop/electron/electron-env.d.ts new file mode 100644 index 0000000..f3b4f4a --- /dev/null +++ b/apps/desktop/electron/electron-env.d.ts @@ -0,0 +1,27 @@ +/// + +declare namespace NodeJS { + interface ProcessEnv { + /** + * The built directory structure + * + * ```tree + * ├─┬─┬ dist + * │ │ └── index.html + * │ │ + * │ ├─┬ dist-electron + * │ │ ├── main.js + * │ │ └── preload.js + * │ + * ``` + */ + APP_ROOT: string + /** /dist/ or /public/ */ + VITE_PUBLIC: string + } +} + +// Used in Renderer process, expose in `preload.ts` +interface Window { + ipcRenderer: import('electron').IpcRenderer +} diff --git a/apps/desktop/electron/ipcEvent/index.ts b/apps/desktop/electron/ipcEvent/index.ts new file mode 100644 index 0000000..88782cf --- /dev/null +++ b/apps/desktop/electron/ipcEvent/index.ts @@ -0,0 +1 @@ +export const EVENT_PARSE_DICOM = "PARSE_DICOM"; diff --git a/apps/desktop/electron/ipcMainHandlers.ts b/apps/desktop/electron/ipcMainHandlers.ts new file mode 100644 index 0000000..53d30a2 --- /dev/null +++ b/apps/desktop/electron/ipcMainHandlers.ts @@ -0,0 +1,51 @@ +import { dialog, ipcMain } from "electron"; +import os from "os"; +import { + findDcmFiles, + processFilesInBatches, + structureMetadata, +} from "./core/dicom"; +import { EVENT_PARSE_DICOM } from "./ipcEvent"; +import PythonManager from "./core/PythonManager"; + +/** + * 渲染进程和主进程的事件调度 + */ +const registerIpcMainHandlers = ( + mainWindow: Electron.BrowserWindow | null, + pythonManager: PythonManager +) => { + if (!mainWindow) return; + + ipcMain.removeAllListeners(); + + /** + * 等待渲染完成再显示窗口 + */ + ipcMain.on("ipc-loaded", () => mainWindow.show()); + + /** + * 解析dicoM + */ + ipcMain.on(EVENT_PARSE_DICOM, async (event, file: string) => { + const dirDialog = await dialog.showOpenDialog(mainWindow, { + properties: ["openDirectory"], + }); + if (dirDialog.filePaths.length > 0) { + const filePaths = await findDcmFiles(dirDialog.filePaths[0]); + const batchSize = os.cpus().length * 1 || 10; + console.time("分批处理"); + const unraw = await processFilesInBatches(filePaths, batchSize); + console.timeEnd("分批处理"); + const result = structureMetadata(unraw); + event.reply(EVENT_PARSE_DICOM + ":RES", result); + } + }); + + ipcMain.on("python-service", (event, data) => { + const { running } = data; + running ? pythonManager.startFlask() : pythonManager.stopFlask(); + }); +}; + +export default registerIpcMainHandlers; diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts new file mode 100644 index 0000000..452abe4 --- /dev/null +++ b/apps/desktop/electron/main.ts @@ -0,0 +1,149 @@ +import { + app, + BrowserWindow, + Tray, + Menu, + globalShortcut, + nativeImage, +} from "electron"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import registerIpcMainHandlers from "./ipcMainHandlers"; +import PythonManager from "./core/PythonManager"; +import "./core/db"; + +const require = createRequire(import.meta.url); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +process.env.APP_ROOT = path.join(__dirname, ".."); + +export const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; +export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); +export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); + +process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL + ? path.join(process.env.APP_ROOT, "public") + : RENDERER_DIST; + +let win: BrowserWindow | null; +let tray: Tray | null = null; +let pythonManager: PythonManager | null; +const theme: "dark" | "light" = "light"; + +const themeTitleBarStyles = { + dark: { color: "rgb(32,32,32)", symbolColor: "#fff" }, + light: {}, +}; + +function createWindow() { + win = new BrowserWindow({ + width: 1280, + height: 800, + show: false, // 先隐藏。等待渲染完成,防止闪烁 + icon: path.join(process.env.VITE_PUBLIC, "AI.png"), + // frame: false, + titleBarStyle: "hidden", // customButtonsOnHover || hidden || hiddenInset + titleBarOverlay: { height: 36, ...themeTitleBarStyles[theme] }, // 渲染进程发消息动态改变这个 + webPreferences: { + preload: path.join(__dirname, "preload.mjs"), + nodeIntegration: true, + }, + }); + + // Test active push message to Renderer-process. + win.webContents.on("did-finish-load", () => { + win?.webContents.send("main-process-message", { + platform: process.platform === "darwin" ? "macos" : "windows", + theme, + }); + }); + + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html")); + } + + pythonManager = new PythonManager(win, "http://127.0.0.1:15001", 3000); + registerIpcMainHandlers(win, pythonManager); +} + +function createTray() { + if (tray) tray.destroy(); + const iconPath = path.join(process.env.VITE_PUBLIC, "AI.png"); // 使用 PNG 图标 + const icon = nativeImage + .createFromPath(iconPath) + .resize({ width: 20, height: 20 }); + tray = new Tray(icon); + + const contextMenu = Menu.buildFromTemplate([ + { + label: "Show App", + click: function () { + if (win) { + win.show(); + } + }, + }, + { + label: "Quit", + click: function () { + app.quit(); + }, + }, + ]); + + tray.setToolTip("My Electron App"); + tray.setContextMenu(contextMenu); + + tray.on("click", () => { + if (win) { + win.isVisible() ? win.hide() : win.show(); + } + }); +} + +function registerGlobalShortcuts() { + // 注册全局快捷键 'CommandOrControl+Shift+S' 来显示应用窗口 + globalShortcut.register("Option+N", () => { + if (win) { + win.isVisible() ? win.hide() : win.show(); + } + }); +} + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + win = null; + } +}); + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +app.on("before-quit", () => { + if (pythonManager?.flaskProcess) pythonManager?.stopFlask(); +}); + +app.whenReady().then(() => { + createWindow(); + createTray(); + registerGlobalShortcuts(); + + // 设置 Dock 图标 + if (process.platform === "darwin") { + const dockIconPath = path.join(process.env.VITE_PUBLIC, "girl.png"); + const dockIcon = nativeImage.createFromPath(dockIconPath); + app.dock.setIcon(dockIcon); + } +}); + +// 注销全局快捷键,当应用退出时 +app.on("will-quit", () => { + globalShortcut.unregisterAll(); +}); diff --git a/apps/desktop/electron/preload.ts b/apps/desktop/electron/preload.ts new file mode 100644 index 0000000..ee7474c --- /dev/null +++ b/apps/desktop/electron/preload.ts @@ -0,0 +1,23 @@ +import { ipcRenderer, contextBridge } from "electron"; + +// --------- Expose some API to the Renderer process --------- +contextBridge.exposeInMainWorld("ipcRenderer", { + on(...args: Parameters) { + const [channel, listener] = args; + return ipcRenderer.on(channel, (event, ...args) => + listener(event, ...args) + ); + }, + off(...args: Parameters) { + const [channel, ...omit] = args; + return ipcRenderer.off(channel, ...omit); + }, + send(...args: Parameters) { + const [channel, ...omit] = args; + return ipcRenderer.send(channel, ...omit); + }, + invoke(...args: Parameters) { + const [channel, ...omit] = args; + return ipcRenderer.invoke(channel, ...omit); + }, +}); diff --git a/apps/desktop/index.html b/apps/desktop/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/apps/desktop/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000..78fe14a --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,78 @@ +{ + "name": "@cvpilot/desktop", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "dist-electron/main.js", + "scripts": { + "dev": "vite", + "build": "tsc && vite build && electron-builder", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/icons": "^5.4.0", + "@google-cloud/spanner": "^7.12.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-menubar": "^1.1.1", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-tooltip": "^1.1.2", + "@tanstack/react-table": "^8.20.5", + "@types/react-icons": "^3.0.0", + "@xenova/transformers": "^2.17.2", + "antd": "^5.20.0", + "better-sqlite3": "^11.1.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "custom-electron-titlebar": "^4.2.8", + "date-fns": "^3.6.0", + "dexie": "^4.0.8", + "dicom-parser": "1.8.21", + "dockview": "^1.15.2", + "electron-store": "^10.0.0", + "flexlayout-react": "^0.7.15", + "framer-motion": "^11.3.24", + "lowdb": "^7.0.1", + "lucide-react": "^0.408.0", + "object-hash": "^3.0.0", + "onnxruntime-node": "^1.18.0", + "openvino-node": "2024.3.0", + "react": "^18.2.0", + "react-day-picker": "^8.10.1", + "react-desktop": "^0.3.9", + "react-dom": "^18.2.0", + "react-icons": "^5.2.1", + "react-resizable-panels": "^2.0.20", + "react-router-dom": "^6.26.0", + "tailwind-merge": "^2.4.0", + "tailwindcss-animate": "^1.0.7", + "react-dropzone": "14.2.3" + }, + "devDependencies": { + "@radix-ui/react-icons": "^1.3.0", + "@types/react": "^18.2.64", + "@types/react-dom": "^18.2.21", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.19", + "electron": "30.4.0", + "electron-builder": "^24.13.3", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.4", + "typescript": "^5.2.2", + "vite": "^5.1.6", + "vite-plugin-electron": "^0.28.6", + "vite-plugin-electron-renderer": "^0.14.5" + } +} \ No newline at end of file diff --git a/apps/desktop/postcss.config.js b/apps/desktop/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/apps/desktop/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/apps/desktop/public/AI.png b/apps/desktop/public/AI.png new file mode 100644 index 0000000..6c86b5d Binary files /dev/null and b/apps/desktop/public/AI.png differ diff --git a/apps/desktop/public/banner.png b/apps/desktop/public/banner.png new file mode 100644 index 0000000..2843c68 Binary files /dev/null and b/apps/desktop/public/banner.png differ diff --git a/apps/desktop/public/flask_app b/apps/desktop/public/flask_app new file mode 100755 index 0000000..996e2fb Binary files /dev/null and b/apps/desktop/public/flask_app differ diff --git a/apps/desktop/public/girl.png b/apps/desktop/public/girl.png new file mode 100644 index 0000000..eedeaab Binary files /dev/null and b/apps/desktop/public/girl.png differ diff --git a/apps/desktop/public/girl.svg b/apps/desktop/public/girl.svg new file mode 100644 index 0000000..dda299c --- /dev/null +++ b/apps/desktop/public/girl.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/public/logo.svg b/apps/desktop/public/logo.svg new file mode 100644 index 0000000..0f0d929 --- /dev/null +++ b/apps/desktop/public/logo.svg @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/apps/desktop/public/tray-icon.png b/apps/desktop/public/tray-icon.png new file mode 100644 index 0000000..6d7e5e5 Binary files /dev/null and b/apps/desktop/public/tray-icon.png differ diff --git a/apps/desktop/public/tray-icon.svg b/apps/desktop/public/tray-icon.svg new file mode 100644 index 0000000..6ddb32a --- /dev/null +++ b/apps/desktop/public/tray-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx new file mode 100644 index 0000000..0fcdb47 --- /dev/null +++ b/apps/desktop/src/App.tsx @@ -0,0 +1,37 @@ +import { ThemeProvider } from "@/components/theme-provider"; +import LayoutMain from "@/pages/Layout"; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import Boot from "@/pages/Boot"; +import { Setting } from "./pages/Setting"; +import { useEffect } from "react"; +import { Models } from "./pages/Models"; +import { Tools } from "./pages/Tools"; +import { Datasource } from "./pages/Datasource"; + +function App() { + const theme = document.querySelector("html")!.getAttribute("theme") as + | "dark" + | "light"; + + useEffect(() => { + window.ipcRenderer.send("ipc-loaded"); + }, []); + + return ( + + + + }> + } /> + } /> + } /> + } /> + } /> + + + + + ); +} + +export default App; diff --git a/apps/desktop/src/components/theme-provider.tsx b/apps/desktop/src/components/theme-provider.tsx new file mode 100644 index 0000000..e292000 --- /dev/null +++ b/apps/desktop/src/components/theme-provider.tsx @@ -0,0 +1,73 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +type Theme = "dark" | "light" | "system"; + +type ThemeProviderProps = { + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const initialState: ThemeProviderState = { + theme: "dark", + setTheme: () => null, +}; + +const ThemeProviderContext = createContext(initialState); + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "vite-ui-theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme + ); + + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove("light", "dark"); + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + + root.classList.add(systemTheme); + return; + } + + root.classList.add(theme); + }, [theme]); + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider"); + + return context; +}; diff --git a/apps/desktop/src/components/ui/button.tsx b/apps/desktop/src/components/ui/button.tsx new file mode 100644 index 0000000..0ba4277 --- /dev/null +++ b/apps/desktop/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/apps/desktop/src/components/ui/calendar.tsx b/apps/desktop/src/components/ui/calendar.tsx new file mode 100644 index 0000000..7d9f9ae --- /dev/null +++ b/apps/desktop/src/components/ui/calendar.tsx @@ -0,0 +1,72 @@ +"use client" + +import * as React from "react" +import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons" +import { DayPicker } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +export type CalendarProps = React.ComponentProps + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" + : "[&:has([aria-selected])]:rounded-md" + ), + day: cn( + buttonVariants({ variant: "ghost" }), + "h-8 w-8 p-0 font-normal aria-selected:opacity-100" + ), + day_range_start: "day-range-start", + day_range_end: "day-range-end", + day_selected: + "bg-slate-900 text-slate-50 hover:bg-slate-900 hover:text-slate-50 focus:bg-slate-900 focus:text-slate-50 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50 dark:hover:text-slate-900 dark:focus:bg-slate-50 dark:focus:text-slate-900", + day_today: "bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-50", + day_outside: + "day-outside text-slate-500 opacity-50 aria-selected:bg-slate-100/50 aria-selected:text-slate-500 aria-selected:opacity-30 dark:text-slate-400 dark:aria-selected:bg-slate-800/50 dark:aria-selected:text-slate-400", + day_disabled: "text-slate-500 opacity-50 dark:text-slate-400", + day_range_middle: + "aria-selected:bg-slate-100 aria-selected:text-slate-900 dark:aria-selected:bg-slate-800 dark:aria-selected:text-slate-50", + day_hidden: "invisible", + ...classNames, + }} + components={{ + IconLeft: ({ ...props }) => , + IconRight: ({ ...props }) => , + }} + {...props} + /> + ) +} +Calendar.displayName = "Calendar" + +export { Calendar } diff --git a/apps/desktop/src/components/ui/card.tsx b/apps/desktop/src/components/ui/card.tsx new file mode 100644 index 0000000..77e9fb7 --- /dev/null +++ b/apps/desktop/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/apps/desktop/src/components/ui/checkbox.tsx b/apps/desktop/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..7d2b3c3 --- /dev/null +++ b/apps/desktop/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/apps/desktop/src/components/ui/command.tsx b/apps/desktop/src/components/ui/command.tsx new file mode 100644 index 0000000..30ef57d --- /dev/null +++ b/apps/desktop/src/components/ui/command.tsx @@ -0,0 +1,155 @@ +"use client" + +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { MagnifyingGlassIcon } from "@radix-ui/react-icons" +import { Command as CommandPrimitive } from "cmdk" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/apps/desktop/src/components/ui/dialog.tsx b/apps/desktop/src/components/ui/dialog.tsx new file mode 100644 index 0000000..95b0d38 --- /dev/null +++ b/apps/desktop/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/apps/desktop/src/components/ui/dropdown-menu.tsx b/apps/desktop/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..242b07a --- /dev/null +++ b/apps/desktop/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,205 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/apps/desktop/src/components/ui/input.tsx b/apps/desktop/src/components/ui/input.tsx new file mode 100644 index 0000000..a92b8e0 --- /dev/null +++ b/apps/desktop/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/apps/desktop/src/components/ui/label.tsx b/apps/desktop/src/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/apps/desktop/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/apps/desktop/src/components/ui/menubar.tsx b/apps/desktop/src/components/ui/menubar.tsx new file mode 100644 index 0000000..57288c7 --- /dev/null +++ b/apps/desktop/src/components/ui/menubar.tsx @@ -0,0 +1,240 @@ +"use client" + +import * as React from "react" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" +import * as MenubarPrimitive from "@radix-ui/react-menubar" + +import { cn } from "@/lib/utils" + +const MenubarMenu = MenubarPrimitive.Menu + +const MenubarGroup = MenubarPrimitive.Group + +const MenubarPortal = MenubarPrimitive.Portal + +const MenubarSub = MenubarPrimitive.Sub + +const MenubarRadioGroup = MenubarPrimitive.RadioGroup + +const Menubar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Menubar.displayName = MenubarPrimitive.Root.displayName + +const MenubarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName + +const MenubarSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName + +const MenubarSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName + +const MenubarContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, + ref + ) => ( + + + + ) +) +MenubarContent.displayName = MenubarPrimitive.Content.displayName + +const MenubarItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +MenubarItem.displayName = MenubarPrimitive.Item.displayName + +const MenubarCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName + +const MenubarRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName + +const MenubarLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +MenubarLabel.displayName = MenubarPrimitive.Label.displayName + +const MenubarSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName + +const MenubarShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +MenubarShortcut.displayname = "MenubarShortcut" + +export { + Menubar, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarItem, + MenubarSeparator, + MenubarLabel, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarPortal, + MenubarSubContent, + MenubarSubTrigger, + MenubarGroup, + MenubarSub, + MenubarShortcut, +} diff --git a/apps/desktop/src/components/ui/resizable.tsx b/apps/desktop/src/components/ui/resizable.tsx new file mode 100644 index 0000000..ce15992 --- /dev/null +++ b/apps/desktop/src/components/ui/resizable.tsx @@ -0,0 +1,45 @@ +"use client" + +import { DragHandleDots2Icon } from "@radix-ui/react-icons" +import * as ResizablePrimitive from "react-resizable-panels" + +import { cn } from "@/lib/utils" + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +) + +const ResizablePanel = ResizablePrimitive.Panel + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean +}) => ( + div]:rotate-90 dark:bg-slate-800 dark:focus-visible:ring-slate-300", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+) + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } diff --git a/apps/desktop/src/components/ui/scroll-area.tsx b/apps/desktop/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..0b4a48d --- /dev/null +++ b/apps/desktop/src/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/apps/desktop/src/components/ui/table.tsx b/apps/desktop/src/components/ui/table.tsx new file mode 100644 index 0000000..c0df655 --- /dev/null +++ b/apps/desktop/src/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/apps/desktop/src/components/ui/tabs.tsx b/apps/desktop/src/components/ui/tabs.tsx new file mode 100644 index 0000000..0f4caeb --- /dev/null +++ b/apps/desktop/src/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/apps/desktop/src/components/ui/toast.tsx b/apps/desktop/src/components/ui/toast.tsx new file mode 100644 index 0000000..f315557 --- /dev/null +++ b/apps/desktop/src/components/ui/toast.tsx @@ -0,0 +1,129 @@ +"use client" + +import * as React from "react" +import { Cross2Icon } from "@radix-ui/react-icons" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-slate-200 p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-slate-800", + { + variants: { + variant: { + default: "border bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50", + destructive: + "destructive group border-red-500 bg-red-500 text-slate-50 dark:border-red-900 dark:bg-red-900 dark:text-slate-50", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/apps/desktop/src/components/ui/toaster.tsx b/apps/desktop/src/components/ui/toaster.tsx new file mode 100644 index 0000000..e223385 --- /dev/null +++ b/apps/desktop/src/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/apps/desktop/src/components/ui/tooltip.tsx b/apps/desktop/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..9e74821 --- /dev/null +++ b/apps/desktop/src/components/ui/tooltip.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/apps/desktop/src/components/ui/use-toast.ts b/apps/desktop/src/components/ui/use-toast.ts new file mode 100644 index 0000000..02e111d --- /dev/null +++ b/apps/desktop/src/components/ui/use-toast.ts @@ -0,0 +1,194 @@ +"use client" + +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/apps/desktop/src/lib/db/database.ts b/apps/desktop/src/lib/db/database.ts new file mode 100644 index 0000000..3f7bbbe --- /dev/null +++ b/apps/desktop/src/lib/db/database.ts @@ -0,0 +1,30 @@ +// database.ts +import Dexie from "dexie"; +import { IFriend } from "./models/User"; + +class Database extends Dexie { + public friends: Dexie.Table; // `number` 是主键的类型 + + constructor() { + super("Database"); + this.version(1).stores({ + friends: "++id, name, age", + }); + this.friends = this.table("friends"); + } + + async addFriend(friend: IFriend): Promise { + return await this.friends.add(friend); + } + + async getAllFriends(): Promise { + return await this.friends.toArray(); + } + + async getFriendsYoungerThan(ageLimit: number): Promise { + return await this.friends.where("age").below(ageLimit).toArray(); + } +} + +const db = new Database(); +export default db; diff --git a/apps/desktop/src/lib/db/models/User.ts b/apps/desktop/src/lib/db/models/User.ts new file mode 100644 index 0000000..820a4e4 --- /dev/null +++ b/apps/desktop/src/lib/db/models/User.ts @@ -0,0 +1,5 @@ +export interface IFriend { + id?: number; // 可选,自增主键 + name: string; + age: number; +} diff --git a/apps/desktop/src/lib/utils.ts b/apps/desktop/src/lib/utils.ts new file mode 100644 index 0000000..365058c --- /dev/null +++ b/apps/desktop/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx new file mode 100644 index 0000000..a13a138 --- /dev/null +++ b/apps/desktop/src/main.tsx @@ -0,0 +1,11 @@ +import ReactDOM from "react-dom/client"; +import App from "./App.tsx"; +import "@/style/global.css"; + +window.ipcRenderer.on("main-process-message", (_event, message) => { + const { platform, theme } = message + document.querySelector('html')?.setAttribute('platform', platform) + document.querySelector('html')?.setAttribute('theme', theme) + + ReactDOM.createRoot(document.getElementById("root")!).render(); +}); diff --git a/apps/desktop/src/pages/Boot/index.tsx b/apps/desktop/src/pages/Boot/index.tsx new file mode 100644 index 0000000..a905899 --- /dev/null +++ b/apps/desktop/src/pages/Boot/index.tsx @@ -0,0 +1,83 @@ +import { Button } from "@/components/ui/button"; +import { motion } from "framer-motion"; +import { useEffect, useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { IoCheckmarkCircleSharp } from "react-icons/io5"; + +const Boot = () => { + const [flaskWaiting, setFlaskWaiting] = useState(false); + const [flaskRunning, setflaskRunning] = useState(false); + const [flaskInfo, setFlaskInfo] = useState(""); + + const handleBootPythonServer = () => { + if (!flaskRunning) setFlaskWaiting(true); + window.ipcRenderer.send("python-service", { running: !flaskRunning }); + }; + + useEffect(() => { + window.ipcRenderer.on("flask-service:response", (event, data) => { + console.log(data); + if (data.running) { + setflaskRunning(true); + setFlaskWaiting(false); + } + if (data.stdout || data.stderr) { + setflaskRunning(true); + setFlaskWaiting(false); + setFlaskInfo(data.stderr); + } + if (data.exited) { + setFlaskInfo(""); + setflaskRunning(false); + } + }); + }, []); + + return ( + +
+
+
+
+
{flaskInfo}
+
+
+
+ +
+
+
+
+ ); +}; + +export default Boot; diff --git a/apps/desktop/src/pages/CommandDialogDemo.tsx b/apps/desktop/src/pages/CommandDialogDemo.tsx new file mode 100644 index 0000000..dc15153 --- /dev/null +++ b/apps/desktop/src/pages/CommandDialogDemo.tsx @@ -0,0 +1,88 @@ +"use client"; + +import * as React from "react"; +import { + CalendarIcon, + EnvelopeClosedIcon, + FaceIcon, + GearIcon, + PersonIcon, + RocketIcon, +} from "@radix-ui/react-icons"; + +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} from "@/components/ui/command"; +import { DialogTitle } from "@radix-ui/react-dialog"; + +export function CommandDialogDemo() { + const [open, setOpen] = React.useState(false); + + React.useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "j" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; + + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + return ( + <> +

+ + J + +

+ + + + + No results found. + + + + Calendar + + + + Search Emoji + + + + Launch + + + + + + + Profile + ⌘P + + + + Mail + ⌘B + + + + Settings + ⌘S + + + + + + ); +} diff --git a/apps/desktop/src/pages/Datasource/FileUpload.tsx b/apps/desktop/src/pages/Datasource/FileUpload.tsx new file mode 100644 index 0000000..2df5c3b --- /dev/null +++ b/apps/desktop/src/pages/Datasource/FileUpload.tsx @@ -0,0 +1,46 @@ +import { Button } from "@/components/ui/button"; +import React from "react"; +import { useDropzone } from "react-dropzone"; + +interface FileUploadProps { + onFilesSelected?: (files: File[]) => void; +} + +const FileUpload: React.FC = ({ onFilesSelected }) => { + const onDrop = (acceptedFiles: File[]) => { + console.log("Accepted files:", acceptedFiles); + // 调用父组件传入的回调函数处理文件 + if (onFilesSelected) { + onFilesSelected(acceptedFiles); + } + }; + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + multiple: true, + accept: { + "file/dicom": [".dcm"], + "file/model": [".stl"], + }, + noClick: false, // 启用点击选择文件 + noKeyboard: true, // 禁用键盘选择(可选) + directory: true, // 启用文件夹上传支持 + }); + + return ( +
+ + {isDragActive ? ( +

把文件/夹放到这里。。。

+ ) : ( +

将文件/夹拖放到此处,或单击选择

+ )} + +
+ ); +}; + +export default FileUpload; diff --git a/apps/desktop/src/pages/Datasource/SeriesTable.tsx b/apps/desktop/src/pages/Datasource/SeriesTable.tsx new file mode 100644 index 0000000..bf36d88 --- /dev/null +++ b/apps/desktop/src/pages/Datasource/SeriesTable.tsx @@ -0,0 +1,316 @@ +"use client"; + +import * as React from "react"; +import { + CaretSortIcon, + ChevronDownIcon, + DotsHorizontalIcon, +} from "@radix-ui/react-icons"; +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +const data: Payment[] = [ + { + id: "m5gr84i9", + amount: 316, + status: "success", + email: "ken99@yahoo.com", + }, + { + id: "3u1reuv4", + amount: 242, + status: "success", + email: "Abe45@gmail.com", + }, + { + id: "derv1ws0", + amount: 837, + status: "processing", + email: "Monserrat44@gmail.com", + }, + { + id: "5kma53ae", + amount: 874, + status: "success", + email: "Silas22@gmail.com", + }, + { + id: "bhqecj4p", + amount: 721, + status: "failed", + email: "carmella@hotmail.com", + }, +]; + +export type Payment = { + id: string; + amount: number; + status: "pending" | "processing" | "success" | "failed"; + email: string; +}; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => ( +
{row.getValue("status")}
+ ), + }, + { + accessorKey: "email", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) =>
{row.getValue("email")}
, + }, + { + accessorKey: "amount", + header: () =>
Amount
, + cell: ({ row }) => { + const amount = parseFloat(row.getValue("amount")); + + // Format the amount as a dollar amount + const formatted = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount); + + return
{formatted}
; + }, + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + const payment = row.original; + + return ( + + + + + + Actions + navigator.clipboard.writeText(payment.id)} + > + Copy payment ID + + + View customer + View payment details + + + ); + }, + }, +]; + +export function SeriesTable() { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+ + table.getColumn("email")?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+ + +
+
+
+ ); +} diff --git a/apps/desktop/src/pages/Datasource/index.tsx b/apps/desktop/src/pages/Datasource/index.tsx new file mode 100644 index 0000000..d9bad78 --- /dev/null +++ b/apps/desktop/src/pages/Datasource/index.tsx @@ -0,0 +1,9 @@ +import FileUpload from "./FileUpload"; + +export const Datasource = () => { + return ( +
+ +
+ ); +}; diff --git a/apps/desktop/src/pages/Dock/index.tsx b/apps/desktop/src/pages/Dock/index.tsx new file mode 100644 index 0000000..cbe4c3c --- /dev/null +++ b/apps/desktop/src/pages/Dock/index.tsx @@ -0,0 +1,122 @@ +import { + DockviewApi, + DockviewReact, + DockviewReadyEvent, + IDockviewPanelProps, +} from "dockview"; +import { useEffect, useState } from "react"; +import "dockview/dist/styles/dockview.css"; +import "./theme.reset.css"; + +const Default = (props: IDockviewPanelProps) => { + return ( +
+
{props.api.title}
+
+ ); +}; + +const components = { + default: Default, +}; + +const Dockview = (props: { theme?: string }) => { + const [disablePanelDrag, setDisablePanelDrag] = useState(false); + const [disableGroupDrag, setDisableGroupDrag] = useState(false); + const [disableOverlay, setDisableOverlay] = useState(false); + + const [api, setApi] = useState(); + + useEffect(() => { + if (!api) return; + + const disposables = [ + api.onWillDragPanel((e) => { + if (!disablePanelDrag) { + return; + } + e.nativeEvent.preventDefault(); + }), + + api.onWillDragGroup((e) => { + if (!disableGroupDrag) { + return; + } + e.nativeEvent.preventDefault(); + }), + api.onWillShowOverlay((e) => { + console.log(e); + + if (!disableOverlay) { + return; + } + + e.preventDefault(); + }), + + api.onWillDrop((e) => { + // + }), + + api.onDidDrop((e) => { + // + }), + ]; + + return () => { + disposables.forEach((disposable) => { + disposable.dispose(); + }); + }; + }, [api, disablePanelDrag, disableGroupDrag, disableOverlay]); + + const onReady = (event: DockviewReadyEvent) => { + setApi(event.api); + + event.api.addPanel({ + id: "panel_1", + component: "default", + }); + + event.api.addPanel({ + id: "panel_2", + component: "default", + + position: { + direction: "right", + referencePanel: "panel_1", + }, + }); + + event.api.addPanel({ + id: "panel_3", + component: "default", + position: { + direction: "below", + referencePanel: "panel_1", + }, + }); + event.api.addPanel({ + id: "panel_4", + component: "default", + }); + event.api.addPanel({ + id: "panel_5", + component: "default", + }); + }; + + return ( +
+
+ +
+
+ ); +}; + +export default Dockview; diff --git a/apps/desktop/src/pages/Dock/theme.reset.css b/apps/desktop/src/pages/Dock/theme.reset.css new file mode 100644 index 0000000..937d3ed --- /dev/null +++ b/apps/desktop/src/pages/Dock/theme.reset.css @@ -0,0 +1,28 @@ +.dockview-theme-light { + --dv-background-color: black; + --dv-paneview-active-outline-color: dodgerblue; + --dv-tabs-and-actions-container-font-size: 13px; + --dv-tabs-and-actions-container-height: 35px; + --dv-drag-over-background-color: hsl(var(--accent)); + --dv-drag-over-border-color: white; + --dv-tabs-container-scrollbar-color: #888; + --dv-icon-hover-background-color: rgba(90, 93, 94, 0.31); + --dv-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5); + --dv-group-view-background-color: white; + --dv-tabs-and-actions-container-background-color: #f3f3f3; + --dv-activegroup-visiblepanel-tab-background-color: white; + --dv-activegroup-hiddenpanel-tab-background-color: #ececec; + --dv-inactivegroup-visiblepanel-tab-background-color: white; + --dv-inactivegroup-hiddenpanel-tab-background-color: #ececec; + --dv-tab-divider-color: white; + --dv-activegroup-visiblepanel-tab-color: rgb(51, 51, 51); + --dv-activegroup-hiddenpanel-tab-color: rgba(51, 51, 51, 0.7); + --dv-inactivegroup-visiblepanel-tab-color: rgba(51, 51, 51, 0.7); + --dv-inactivegroup-hiddenpanel-tab-color: rgba(51, 51, 51, 0.35); + --dv-separator-border: hsl(var(--border)); + --dv-paneview-header-border-color: rgb(51, 51, 51); +} + +.tabs-container .tab{ + border-radius: 5px 5px 0 0; +} \ No newline at end of file diff --git a/apps/desktop/src/pages/FlexLayoutDemo.tsx b/apps/desktop/src/pages/FlexLayoutDemo.tsx new file mode 100644 index 0000000..61ac536 --- /dev/null +++ b/apps/desktop/src/pages/FlexLayoutDemo.tsx @@ -0,0 +1,150 @@ +import React from "react"; +import { + Actions, + BorderNode, + DockLocation, + IJsonModel, + ITabRenderValues, + ITabSetRenderValues, + Layout, + Model, + TabNode, + TabSetNode, +} from "flexlayout-react"; +import "flexlayout-react/style/light.css"; +import "./Layout.css"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { VscAdd } from "react-icons/vsc"; + +const json: IJsonModel = { + global: { + tabEnableFloat: false, + tabSetMinWidth: 150, + tabSetMinHeight: 200, + /** + * 选项卡栏高度 + */ + tabSetTabStripHeight: 32, //tab栏高度 + /** + * 四周边缘定位 + */ + enableEdgeDock: false, + borderMinSize: 100, + splitterSize: 2, + tabSetClassNameTabStrip: "custom-tab-strip", + }, + borders: [], + layout: { + type: "row", + weight: 100, + children: [ + { + type: "tabset", + weight: 50, + children: [ + { + type: "tab", + id: "tab1", + name: "测试22222222222", + component: "tabComponent", + config: { icon: "📘" }, + }, + ], + }, + { + type: "tabset", + weight: 50, + children: [ + { + type: "tab", + id: "tab2", + name: "Two", + component: "tabComponent", + config: { icon: "📘" }, + }, + ], + }, + ], + }, +}; + +const FlexLayoutComponent: React.FC = () => { + const model = Model.fromJson(json); + + const factory = (node: TabNode) => { + const component = node.getComponent(); + if (component === "tabComponent") { + return
{node.getName()}11
; + } + }; + + const onRenderTab = (node: TabNode, renderValues: ITabRenderValues) => { + renderValues.content = ( +
+ {node.getConfig().icon} + {renderValues.content} +
+ ); + }; + + const addTab = (node: TabSetNode | BorderNode) => { + // 创建一个新的 tab 对象 + const newTab = { + type: "tab", + name: "New Tab", + component: "tabComponent", + config: { icon: "" }, + }; + + // 使用 FlexLayout 的 addAction 执行添加 tab 的动作 + const addAction = Actions.addNode( + newTab, + node.getId(), + DockLocation.CENTER, + -1 + ); + node.getModel().doAction(addAction); + }; + + const onRenderTabSet = ( + node: TabSetNode | BorderNode, + renderValues: ITabSetRenderValues + ) => { + const createTabButton = ( + + + + + + +

新建标签页

+
+
+
+ ); + renderValues.stickyButtons.push(createTabButton); + }; + + return ( + `${defaultClass} custom-layout-class`} + onRenderTab={onRenderTab} + onRenderTabSet={onRenderTabSet} + /> + ); +}; + +export default FlexLayoutComponent; diff --git a/apps/desktop/src/pages/Layout.css b/apps/desktop/src/pages/Layout.css new file mode 100644 index 0000000..9c57f74 --- /dev/null +++ b/apps/desktop/src/pages/Layout.css @@ -0,0 +1,18 @@ +/* Layout.css */ +.custom-tab-strip { + border-bottom-color: hsl(var(--border)); + padding-left: 0; + + .flexlayout__tabset_tabbar_inner_tab_container { + border-top: 0; + padding-left: 0; + } + + .flexlayout__tab_button--selected { + background: hsl(var(--secondary)); + } + + .flexlayout__tabset-selected { + background: transparent; + } +} diff --git a/apps/desktop/src/pages/Layout.tsx b/apps/desktop/src/pages/Layout.tsx new file mode 100644 index 0000000..136d9af --- /dev/null +++ b/apps/desktop/src/pages/Layout.tsx @@ -0,0 +1,42 @@ +import { LeftDocker } from "./LeftDocker"; +import { Outlet } from "react-router-dom"; + +const LayoutMain = () => { + const platform = + document.querySelector("html")?.getAttribute("platform") ?? "macos"; + // const titleBarStyles = + // platform === "macos" ? "pl-[5rem] pr-[.5rem]" : "pl-[.5rem]"; + const titleBarStyles = + platform === "macos" ? "px-[.5rem]" : "pl-[.5rem]"; + + return ( +
+
+
+ + CVPilot Tool +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+ ); +}; + +export default LayoutMain; diff --git a/apps/desktop/src/pages/LeftDocker.css b/apps/desktop/src/pages/LeftDocker.css new file mode 100644 index 0000000..e69de29 diff --git a/apps/desktop/src/pages/LeftDocker.tsx b/apps/desktop/src/pages/LeftDocker.tsx new file mode 100644 index 0000000..67cf8c6 --- /dev/null +++ b/apps/desktop/src/pages/LeftDocker.tsx @@ -0,0 +1,55 @@ +import { Link, useLocation } from "react-router-dom"; +import { + IoCubeOutline, + IoHammerOutline, + IoHandLeftOutline, + IoLayersOutline, + IoListOutline, + IoPlayOutline, + IoSettingsOutline +} from "react-icons/io5"; +import './LeftDocker.css' + +import { ReactNode } from 'react'; + +type MenuItem = { + to: string; + name: string; + icon: ReactNode; +}; + + +const menuItems: MenuItem[] = [ + { to: "/", name: "一键启动", icon: }, + { to: "/datasource", name: "数据列表", icon: }, + { to: "/models", name: "模型管理", icon: }, + { to: "/tools", name: "小工具", icon: }, + // { to: "/help", name: "帮助", icon: }, + { to: "/setting", name: "设置", icon: }, +]; + +export const LeftDocker = () => { + const location = useLocation(); + + const handleClick = (item: MenuItem) => { + console.log(`Clicked on ${item.name}`); + }; + + return ( +
    + {menuItems.map((item) => ( +
  • handleClick(item)} + > + + {item.icon} + {item.name} + +
  • + ))} +
+ ); +}; diff --git a/apps/desktop/src/pages/MenuBar.tsx b/apps/desktop/src/pages/MenuBar.tsx new file mode 100644 index 0000000..afa4573 --- /dev/null +++ b/apps/desktop/src/pages/MenuBar.tsx @@ -0,0 +1,126 @@ +import { + Menubar, + MenubarCheckboxItem, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarRadioGroup, + MenubarRadioItem, + MenubarSeparator, + MenubarShortcut, + MenubarSub, + MenubarSubContent, + MenubarSubTrigger, + MenubarTrigger, +} from "@/components/ui/menubar"; +import { EVENT_PARSE_DICOM } from "../../electron/ipcEvent"; +import { useEffect } from "react"; + +export const MenuBar = () => { + useEffect(() => { + window.ipcRenderer.on(EVENT_PARSE_DICOM + ":RES", (event, data) => { + console.log(data); + if (data.error) return; + }); + + return () => { + window.ipcRenderer.off(EVENT_PARSE_DICOM + ":RES", () => { }); + }; + }, []); + + const handleImportDicom = () => { + window.ipcRenderer.send(EVENT_PARSE_DICOM); + }; + + return ( + + + 文件 + + + 导入Dicom⌘T + + + New Window ⌘N + + New Incognito Window + + + Share + + Email link + Messages + Notes + + + + + Print... ⌘P + + + + + Edit + + + Undo ⌘Z + + + Redo ⇧⌘Z + + + + Find + + Search the web + + Find... + Find Next + Find Previous + + + + Cut + Copy + Paste + + + + View + + Always Show Bookmarks Bar + + Always Show Full URLs + + + + Reload ⌘R + + + Force Reload ⇧⌘R + + + Toggle Fullscreen + + Hide Sidebar + + + + Profiles + + + Andy + Benoit + Luis + + + Edit... + + Add Profile... + + + + ); +}; diff --git a/apps/desktop/src/pages/Models/ModelTable.tsx b/apps/desktop/src/pages/Models/ModelTable.tsx new file mode 100644 index 0000000..1f2e5e8 --- /dev/null +++ b/apps/desktop/src/pages/Models/ModelTable.tsx @@ -0,0 +1,328 @@ +import { CaretSortIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"; +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useState } from "react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { IoAdd, IoRefreshCircleOutline } from "react-icons/io5"; + +export type Payment = { + id: string; + filename: string; + modelname: string; + author: string; + homepage: string; + hash: string; + filesize: string; + createtime: string; +}; + +const data: Payment[] = [ + { + id: "m5gr84i9", + filename: "waizhou111", + modelname: "外周111", + author: "ken99@yahoo.com", + homepage: "", + hash: "m5gr84i9", + filesize: "500mb", + createtime: "2024年08月29日", + }, + { + id: "3u1reuv4", + filename: "waizhou222", + modelname: "外周222", + author: "ken99@yahoo.com", + homepage: "", + hash: "m5gr84i9", + filesize: "500mb", + createtime: "2024年01月29日", + }, + { + id: "derv1ws0", + filename: "waizhou333", + modelname: "外周333", + author: "ken99@yahoo.com", + homepage: "", + hash: "m5gr84i9", + filesize: "500mb", + createtime: "2024年08月29日", + }, + { + id: "5kma53ae", + filename: "waizhou1", + modelname: "外周1", + author: "ken99@yahoo.com", + homepage: "", + hash: "m5gr84i9", + filesize: "500mb", + createtime: "2024年08月29日", + }, + { + id: "bhqecj4p", + filename: "waizhou2", + modelname: "外周2", + author: "ken99@yahoo.com", + homepage: "", + hash: "m5gr84i9", + filesize: "500mb", + createtime: "2024年08月29日", + }, + { + id: "1bhqecj4p", + filename: "waizhou3", + modelname: "外周3", + author: "ken99@yahoo.com", + homepage: "", + hash: "m5gr84i9", + filesize: "500mb", + createtime: "2024年08月29日", + }, + { + id: "2bhqecj4p", + filename: "waizhou6", + modelname: "外周6", + author: "ken99@yahoo.com", + homepage: "", + hash: "m5gr84i9", + filesize: "500mb", + createtime: "2024年08月29日", + }, + { + id: "3bhqecj4p", + filename: "waizhou9", + modelname: "外周9", + author: "ken99@yahoo.com", + homepage: "", + hash: "m5gr84i9", + filesize: "500mb", + createtime: "2024年08月29日", + }, +]; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "filename", + header: "文件名", + }, + { + accessorKey: "modelname", + header: "模型名", + }, + { + accessorKey: "author", + header: "作者", + }, + { + accessorKey: "createtime", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
{row.getValue("createtime")}
+ ), + }, + { + accessorKey: "filesize", + header: "文件大小", + }, + { + accessorKey: "homepage", + header: "主页", + }, + { + accessorKey: "hash", + header: "短哈希", + }, + + { + id: "actions", + header: "操作", + enableHiding: false, + cell: ({ row }) => { + const payment = row.original; + return ( + + + + + + navigator.clipboard.writeText(payment.id)} + > + 下载 + + + 删除 + + + ); + }, + }, +]; + +export function ModelTable() { + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + const [rowSelection, setRowSelection] = useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+ + table.getColumn("email")?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> +
+ + +
+
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + 无结果 + + + )} + +
+
+
+
+ 选中了 {table.getFilteredSelectedRowModel().rows.length} +
+
+
+
+ ); +} diff --git a/apps/desktop/src/pages/Models/index.tsx b/apps/desktop/src/pages/Models/index.tsx new file mode 100644 index 0000000..d14fe80 --- /dev/null +++ b/apps/desktop/src/pages/Models/index.tsx @@ -0,0 +1,5 @@ +import { ModelTable } from "./ModelTable"; + +export const Models = () => { + return ; +}; diff --git a/apps/desktop/src/pages/Setting/index.tsx b/apps/desktop/src/pages/Setting/index.tsx new file mode 100644 index 0000000..e470494 --- /dev/null +++ b/apps/desktop/src/pages/Setting/index.tsx @@ -0,0 +1,12 @@ +import { motion } from 'framer-motion'; + +export const Setting = () => { + return +
设置
+
+} \ No newline at end of file diff --git a/apps/desktop/src/pages/Tools/index.tsx b/apps/desktop/src/pages/Tools/index.tsx new file mode 100644 index 0000000..ad1c9a2 --- /dev/null +++ b/apps/desktop/src/pages/Tools/index.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const Tools = () => { + return
Tools
; +}; diff --git a/apps/desktop/src/style/global.css b/apps/desktop/src/style/global.css new file mode 100644 index 0000000..db0acf5 --- /dev/null +++ b/apps/desktop/src/style/global.css @@ -0,0 +1,78 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body, +#root { + height: 100%; + margin: 0; + overflow: hidden; +} + +@layer base { + :root { + --background: 0 0% 94.12%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --background: 0deg 0% 12.55%; + --foreground: 0 0% 100%; + --card: 0 0% 14.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/apps/desktop/src/vite-env.d.ts b/apps/desktop/src/vite-env.d.ts new file mode 100644 index 0000000..151aa68 --- /dev/null +++ b/apps/desktop/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/apps/desktop/tailwind.config.js b/apps/desktop/tailwind.config.js new file mode 100644 index 0000000..e8e20be --- /dev/null +++ b/apps/desktop/tailwind.config.js @@ -0,0 +1,86 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate"), function ({ addUtilities }) { + addUtilities({ + '.drag': { + '-webkit-app-region': 'drag', + }, + '.no-drag': { + '-webkit-app-region': 'no-drag', + }, + }); + },], +} \ No newline at end of file diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json new file mode 100644 index 0000000..0bd9955 --- /dev/null +++ b/apps/desktop/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "include": [ + "src", + "electron" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} \ No newline at end of file diff --git a/apps/desktop/tsconfig.node.json b/apps/desktop/tsconfig.node.json new file mode 100644 index 0000000..97ede7e --- /dev/null +++ b/apps/desktop/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts new file mode 100644 index 0000000..11fed98 --- /dev/null +++ b/apps/desktop/vite.config.ts @@ -0,0 +1,35 @@ +import { defineConfig } from "vite"; +import path from "node:path"; +import electron from "vite-plugin-electron/simple"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + electron({ + main: { + // Shortcut of `build.lib.entry`. + entry: "electron/main.ts", + }, + preload: { + // Shortcut of `build.rollupOptions.input`. + // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`. + input: path.join(__dirname, "electron/preload.ts"), + }, + // Ployfill the Electron and Node.js API for Renderer process. + // If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process. + // See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer + renderer: + process.env.NODE_ENV === "test" + ? // https://github.com/electron-vite/vite-plugin-electron-renderer/issues/78#issuecomment-2053600808 + undefined + : {}, + }), + ], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +});