mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: two level windows and self-managed vite project
This commit is contained in:
+36
-5
@@ -2,11 +2,21 @@
|
||||
|
||||
Memoh desktop application built with [electron-vite](https://electron-vite.github.io/).
|
||||
|
||||
The renderer is intentionally a thin shell — its `main.ts` imports `@memohai/web`'s own
|
||||
bootstrap (router / Pinia / api-client / `App.vue`) so the desktop app runs the same
|
||||
experience as the web app out of the box. Future desktop-only customization should
|
||||
happen in this package (by replacing or composing parts of the `@memohai/web` surface),
|
||||
not by forking the web app.
|
||||
The renderer owns its own `src/renderer/src/main.ts` — it imports the reusable
|
||||
building blocks from `@memohai/web` (`App.vue`, `router`, `i18n`, `api-client`,
|
||||
`style.css`) but assembles the Vue app locally. Desktop-only customization (extra
|
||||
Pinia plugins, Electron-specific stores / providers, alternate routers, etc.)
|
||||
belongs in this `main.ts`, not in `@memohai/web`.
|
||||
|
||||
### How the reuse is wired
|
||||
|
||||
- `@memohai/web/package.json` exposes `App.vue`, `router`, `i18n`, `api-client`,
|
||||
and `style.css` through its `exports` field.
|
||||
- Vite (via `electron.vite.config.ts`) resolves those subpaths to the real
|
||||
files in `apps/web/src/` at bundle time.
|
||||
- `vue-tsc` is pointed at local type stubs in `src/renderer/types/web-stubs.d.ts`
|
||||
via tsconfig `paths`, so desktop's typecheck does **not** descend into
|
||||
`apps/web/src/` or `packages/ui/src/` (those have their own CI).
|
||||
|
||||
## Development
|
||||
|
||||
@@ -28,3 +38,24 @@ pnpm --filter @memohai/desktop build:dir # unpacked app dir (CI smoke test
|
||||
```
|
||||
|
||||
Output goes to `apps/desktop/dist/`.
|
||||
|
||||
## Icons
|
||||
|
||||
All app icons are generated from `apps/web/public/logo.svg` (the brand mark) by
|
||||
`scripts/build-icons.mjs`. Re-run after the logo changes:
|
||||
|
||||
```bash
|
||||
pnpm --filter @memohai/desktop icons
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `build/icon.icns` | macOS bundle icon (16…1024 + @2x) — packaged into `.app/Contents/Resources/` |
|
||||
| `build/icon.ico` | Windows installer / `.exe` icon (16/24/32/48/64/128/256) |
|
||||
| `build/icon.png` | Linux `.deb`/`.rpm`/`.AppImage` icon (1024×1024) |
|
||||
| `resources/icon.png` | Runtime `BrowserWindow.icon` + macOS dev `app.dock.setIcon` (512×512); bundled via `asarUnpack` |
|
||||
|
||||
`build/icon.icns` requires macOS (`iconutil`); the script skips it on other
|
||||
platforms.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 364 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 36 KiB |
@@ -78,6 +78,7 @@ export default defineConfig(({ command }) => {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, 'src/renderer/index.html'),
|
||||
settings: resolve(__dirname, 'src/renderer/settings.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"version": "0.7.1",
|
||||
"type": "module",
|
||||
"description": "Memoh Electron desktop application (thin shell reusing @memohai/web)",
|
||||
"description": "Memoh Electron desktop application (self-managed bootstrap reusing @memohai/web components)",
|
||||
"main": "./out/main/index.js",
|
||||
"scripts": {
|
||||
"dev": "electron-vite dev",
|
||||
@@ -16,7 +16,8 @@
|
||||
"build:win": "electron-vite build && electron-builder --win",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web"
|
||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
||||
"icons": "node scripts/build-icons.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
@@ -29,15 +30,27 @@
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@memohai/config": "workspace:*",
|
||||
"@pinia/colada": "^0.21.2",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"animate.css": "^4.1.1",
|
||||
"electron": "^34.5.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-vite": "^4.0.0",
|
||||
"katex": "^0.16.37",
|
||||
"markstream-vue": "0.0.7-beta.2",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"png-to-ico": "^3.0.1",
|
||||
"sharp": "^0.34.5",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.1",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-sonner": "^2.0.9",
|
||||
"vue-tsc": "^3.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
// Regenerate every desktop icon asset from apps/web/public/logo.svg.
|
||||
//
|
||||
// Outputs:
|
||||
// build/icon.png 1024x1024 (electron-builder linux + canonical raster)
|
||||
// build/icon.icns multi-resolution macOS bundle icon
|
||||
// build/icon.ico multi-resolution Windows icon
|
||||
// resources/icon.png 512x512 runtime BrowserWindow.icon / dock.setIcon
|
||||
//
|
||||
// Run: pnpm --filter @memohai/desktop icons
|
||||
//
|
||||
// The brand logo is non-square (~1.135:1); we render it centered onto a
|
||||
// transparent square canvas with PADDING_RATIO of headroom on each side so
|
||||
// macOS Big Sur+ squircle masks and Windows 1px borders don't clip the mark.
|
||||
|
||||
import { execFile } from 'node:child_process'
|
||||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
|
||||
import pngToIco from 'png-to-ico'
|
||||
import sharp from 'sharp'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(__dirname, '..')
|
||||
const SRC_SVG = resolve(ROOT, '../web/public/logo.svg')
|
||||
const BUILD_DIR = resolve(ROOT, 'build')
|
||||
const RESOURCES_DIR = resolve(ROOT, 'resources')
|
||||
|
||||
const MASTER_SIZE = 1024
|
||||
// 14% padding on each side ≈ matches Apple's ~14% safe area for icon glyphs.
|
||||
const PADDING_RATIO = 0.14
|
||||
|
||||
const ICONSET_SIZES = [
|
||||
{ name: 'icon_16x16.png', size: 16 },
|
||||
{ name: 'icon_16x16@2x.png', size: 32 },
|
||||
{ name: 'icon_32x32.png', size: 32 },
|
||||
{ name: 'icon_32x32@2x.png', size: 64 },
|
||||
{ name: 'icon_128x128.png', size: 128 },
|
||||
{ name: 'icon_128x128@2x.png', size: 256 },
|
||||
{ name: 'icon_256x256.png', size: 256 },
|
||||
{ name: 'icon_256x256@2x.png', size: 512 },
|
||||
{ name: 'icon_512x512.png', size: 512 },
|
||||
{ name: 'icon_512x512@2x.png', size: 1024 },
|
||||
]
|
||||
|
||||
const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256]
|
||||
|
||||
async function renderMaster() {
|
||||
const svg = await readFile(SRC_SVG)
|
||||
// Derive `inset` from `margin` (not the other way) to guarantee
|
||||
// inset + 2*margin === MASTER_SIZE exactly, avoiding off-by-one.
|
||||
const margin = Math.floor(MASTER_SIZE * PADDING_RATIO)
|
||||
const inset = MASTER_SIZE - margin * 2
|
||||
|
||||
const inner = await sharp(svg, { density: 600 })
|
||||
.resize(inset, inset, {
|
||||
fit: 'contain',
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
})
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
return sharp(inner)
|
||||
.extend({
|
||||
top: margin,
|
||||
bottom: margin,
|
||||
left: margin,
|
||||
right: margin,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
})
|
||||
.png()
|
||||
.toBuffer()
|
||||
}
|
||||
|
||||
async function writeResized(master, size, outPath) {
|
||||
await sharp(master).resize(size, size, { fit: 'contain' }).png().toFile(outPath)
|
||||
}
|
||||
|
||||
async function buildIcns(master) {
|
||||
const iconset = resolve(BUILD_DIR, 'icon.iconset')
|
||||
await rm(iconset, { recursive: true, force: true })
|
||||
await mkdir(iconset, { recursive: true })
|
||||
|
||||
await Promise.all(
|
||||
ICONSET_SIZES.map(({ name, size }) =>
|
||||
writeResized(master, size, resolve(iconset, name)),
|
||||
),
|
||||
)
|
||||
|
||||
await execFileAsync('iconutil', [
|
||||
'-c', 'icns',
|
||||
iconset,
|
||||
'-o', resolve(BUILD_DIR, 'icon.icns'),
|
||||
])
|
||||
await rm(iconset, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
async function buildIco(master) {
|
||||
const buffers = await Promise.all(
|
||||
ICO_SIZES.map(size =>
|
||||
sharp(master).resize(size, size, { fit: 'contain' }).png().toBuffer(),
|
||||
),
|
||||
)
|
||||
const ico = await pngToIco(buffers)
|
||||
await writeFile(resolve(BUILD_DIR, 'icon.ico'), ico)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await mkdir(BUILD_DIR, { recursive: true })
|
||||
await mkdir(RESOURCES_DIR, { recursive: true })
|
||||
|
||||
console.log(`source: ${SRC_SVG}`)
|
||||
const master = await renderMaster()
|
||||
|
||||
await Promise.all([
|
||||
sharp(master).png().toFile(resolve(BUILD_DIR, 'icon.png')),
|
||||
sharp(master).resize(512, 512, { fit: 'contain' }).png()
|
||||
.toFile(resolve(RESOURCES_DIR, 'icon.png')),
|
||||
])
|
||||
console.log(' -> build/icon.png (1024)')
|
||||
console.log(' -> resources/icon.png (512)')
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
await buildIcns(master)
|
||||
console.log(' -> build/icon.icns (16…1024 + @2x)')
|
||||
} else {
|
||||
console.warn(' ! skipping icon.icns: iconutil only available on macOS')
|
||||
}
|
||||
|
||||
await buildIco(master)
|
||||
console.log(` -> build/icon.ico (${ICO_SIZES.join('/')})`)
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
+103
-25
@@ -1,23 +1,47 @@
|
||||
import { app, shell, BrowserWindow } from 'electron'
|
||||
import { app, shell, BrowserWindow, ipcMain } from 'electron'
|
||||
import { join } from 'node:path'
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||
import iconPng from '../../resources/icon.png?asset'
|
||||
|
||||
const DEFAULT_WIDTH = 1280
|
||||
const DEFAULT_HEIGHT = 800
|
||||
const MIN_WIDTH = 960
|
||||
const MIN_HEIGHT = 600
|
||||
const CHAT_DEFAULTS = { width: 1280, height: 800, minWidth: 960, minHeight: 600 }
|
||||
const SETTINGS_DEFAULTS = { width: 1080, height: 720, minWidth: 880, minHeight: 560 }
|
||||
|
||||
function createWindow(): BrowserWindow {
|
||||
type WindowKind = 'chat' | 'settings'
|
||||
|
||||
let chatWindow: BrowserWindow | null = null
|
||||
let settingsWindow: BrowserWindow | null = null
|
||||
|
||||
function applyExternalLinkHandler(window: BrowserWindow): void {
|
||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
}
|
||||
|
||||
function loadRendererEntry(window: BrowserWindow, entry: 'index' | 'settings'): void {
|
||||
const base = process.env.ELECTRON_RENDERER_URL
|
||||
if (is.dev && base) {
|
||||
window.loadURL(`${base}/${entry}.html`)
|
||||
return
|
||||
}
|
||||
window.loadFile(join(__dirname, `../renderer/${entry}.html`))
|
||||
}
|
||||
|
||||
// `electron-vite` emits the preload bundle as `index.mjs` because the
|
||||
// package is ESM (`"type": "module"`). Electron silently no-ops if this
|
||||
// path doesn't exist — keeping the file name in sync with the build
|
||||
// output is what wires the IPC bridge into the renderer.
|
||||
const PRELOAD_FILE = '../preload/index.mjs'
|
||||
|
||||
function createChatWindow(): BrowserWindow {
|
||||
const window = new BrowserWindow({
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
minWidth: MIN_WIDTH,
|
||||
minHeight: MIN_HEIGHT,
|
||||
...CHAT_DEFAULTS,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
title: 'Memoh',
|
||||
icon: iconPng,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
preload: join(__dirname, PRELOAD_FILE),
|
||||
sandbox: false,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
@@ -26,34 +50,88 @@ function createWindow(): BrowserWindow {
|
||||
|
||||
window.on('ready-to-show', () => {
|
||||
window.show()
|
||||
if (is.dev) window.webContents.openDevTools({ mode: 'detach' })
|
||||
})
|
||||
window.on('closed', () => {
|
||||
chatWindow = null
|
||||
})
|
||||
|
||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||
window.loadURL(process.env.ELECTRON_RENDERER_URL)
|
||||
} else {
|
||||
window.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
applyExternalLinkHandler(window)
|
||||
loadRendererEntry(window, 'index')
|
||||
return window
|
||||
}
|
||||
|
||||
function createSettingsWindow(parent: BrowserWindow | null): BrowserWindow {
|
||||
const window = new BrowserWindow({
|
||||
...SETTINGS_DEFAULTS,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
title: 'Memoh · Settings',
|
||||
icon: iconPng,
|
||||
parent: parent ?? undefined,
|
||||
// Not modal — user can still interact with the chat window while settings is open.
|
||||
modal: false,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, PRELOAD_FILE),
|
||||
sandbox: false,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
})
|
||||
|
||||
window.on('ready-to-show', () => {
|
||||
window.show()
|
||||
})
|
||||
window.on('closed', () => {
|
||||
settingsWindow = null
|
||||
})
|
||||
|
||||
applyExternalLinkHandler(window)
|
||||
loadRendererEntry(window, 'settings')
|
||||
return window
|
||||
}
|
||||
|
||||
function ensureWindow(kind: WindowKind): BrowserWindow {
|
||||
if (kind === 'chat') {
|
||||
if (!chatWindow || chatWindow.isDestroyed()) chatWindow = createChatWindow()
|
||||
return chatWindow
|
||||
}
|
||||
if (!settingsWindow || settingsWindow.isDestroyed()) {
|
||||
settingsWindow = createSettingsWindow(chatWindow)
|
||||
}
|
||||
return settingsWindow
|
||||
}
|
||||
|
||||
function focusWindow(window: BrowserWindow): void {
|
||||
if (window.isMinimized()) window.restore()
|
||||
window.show()
|
||||
window.focus()
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
electronApp.setAppUserModelId('ai.memoh.desktop')
|
||||
|
||||
if (process.platform === 'darwin' && app.dock) {
|
||||
app.dock.setIcon(iconPng)
|
||||
}
|
||||
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
createWindow()
|
||||
ipcMain.handle('window:open-settings', () => {
|
||||
focusWindow(ensureWindow('settings'))
|
||||
})
|
||||
ipcMain.handle('window:close-self', (event) => {
|
||||
const sender = BrowserWindow.fromWebContents(event.sender)
|
||||
sender?.close()
|
||||
})
|
||||
|
||||
chatWindow = createChatWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
chatWindow = createChatWindow()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import type { MemohApi } from './index'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: Record<string, never>
|
||||
api: MemohApi
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { contextBridge } from 'electron'
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
// Renderer-facing API surface. Intentionally minimal for now — extend here when
|
||||
// the desktop shell needs to expose native capabilities (window control, file
|
||||
// dialogs, deep links, etc.).
|
||||
const api = {}
|
||||
// Renderer-facing API surface. Keep this intentionally small — it is the
|
||||
// full security boundary between chromium renderer processes and the
|
||||
// node-privileged main process.
|
||||
const api = {
|
||||
window: {
|
||||
openSettings: (): Promise<void> => ipcRenderer.invoke('window:open-settings'),
|
||||
closeSelf: (): Promise<void> => ipcRenderer.invoke('window:close-self'),
|
||||
},
|
||||
}
|
||||
|
||||
export type MemohApi = typeof api
|
||||
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
@@ -14,8 +21,6 @@ if (process.contextIsolated) {
|
||||
console.error(error)
|
||||
}
|
||||
} else {
|
||||
// @ts-expect-error — fall-through for sandbox-less builds
|
||||
window.electron = electronAPI
|
||||
// @ts-expect-error — fall-through for sandbox-less builds
|
||||
window.api = api
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Memoh · Settings</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/settings.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
import { Toaster } from '@memohai/ui'
|
||||
import 'vue-sonner/style.css'
|
||||
import { useSettingsStore } from '@memohai/web/store/settings'
|
||||
|
||||
useSettingsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="[&_input]:shadow-none!">
|
||||
<RouterView />
|
||||
<Toaster position="top-center" />
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
import { createRouter, createMemoryHistory, type RouteLocationNormalized } from 'vue-router'
|
||||
|
||||
// Chat-window router. Owns ONLY chat-related routes — visiting `/settings`
|
||||
// (e.g. via the chat sidebar's settings button reused from @memohai/web)
|
||||
// is intercepted in a navigation guard and forwarded to the main process,
|
||||
// which opens the dedicated settings BrowserWindow instead of routing
|
||||
// in-place. Memory history matches Electron's file:// runtime cleanly and
|
||||
// keeps the URL bar irrelevant.
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@memohai/web/pages/main-section/index.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'home',
|
||||
path: '',
|
||||
component: () => import('@memohai/web/pages/home/index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'chat',
|
||||
path: '/chat/:botId?/:sessionId?',
|
||||
component: () => import('@memohai/web/pages/home/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Login',
|
||||
path: '/login',
|
||||
component: () => import('@memohai/web/pages/login/index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'oauth-mcp-callback',
|
||||
path: '/oauth/mcp/callback',
|
||||
component: () => import('@memohai/web/pages/oauth/mcp-callback.vue'),
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
router.onError((error: Error) => {
|
||||
const isChunkLoadError =
|
||||
error.message.includes('Failed to fetch dynamically imported module') ||
|
||||
error.message.includes('Importing a module script failed') ||
|
||||
error.message.includes('error loading dynamically imported module')
|
||||
if (isChunkLoadError) {
|
||||
console.warn('[Router] Chunk load failed, reloading...', error.message)
|
||||
window.location.reload()
|
||||
return
|
||||
}
|
||||
throw error
|
||||
})
|
||||
|
||||
router.beforeEach((to: RouteLocationNormalized) => {
|
||||
// Settings lives in its own BrowserWindow. Any in-app `router.push('/settings')`
|
||||
// (e.g. from @memohai/web's chat sidebar) is hijacked here and forwarded to
|
||||
// the main process. Returning `false` aborts the navigation so the chat
|
||||
// window stays where it was — must happen unconditionally, otherwise the
|
||||
// router falls through to the auth check below and bounces to `/login`.
|
||||
if (to.path === '/settings' || to.path.startsWith('/settings/')) {
|
||||
const openSettings = window.api?.window?.openSettings
|
||||
if (typeof openSettings === 'function') {
|
||||
void openSettings()
|
||||
} else {
|
||||
// Most common cause: a long-running `electron-vite dev` session is
|
||||
// serving a renderer page paired with a preload bundle that pre-dates
|
||||
// the IPC surface. Restart the dev process or reload the window.
|
||||
console.warn(
|
||||
'[chat-router] window.api.window.openSettings unavailable; ' +
|
||||
'preload may be stale (restart electron-vite dev) or running outside Electron',
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
if (to.fullPath === '/login') {
|
||||
return token ? { path: '/' } : true
|
||||
}
|
||||
if (to.path.startsWith('/oauth/')) {
|
||||
return true
|
||||
}
|
||||
return token ? true : { name: 'Login' }
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,5 +1,30 @@
|
||||
// Thin desktop renderer entry — defers the full bootstrap (Pinia, router,
|
||||
// i18n, api-client, App.vue) to @memohai/web. When the desktop shell needs
|
||||
// to diverge, replace this import with an inline copy of web's main.ts and
|
||||
// customize as needed.
|
||||
import '@memohai/web/main'
|
||||
// Chat-window renderer entry. Owns its bootstrap chain so desktop can layer
|
||||
// on Electron-specific plugins / stores / providers without touching
|
||||
// @memohai/web. Pairs with `src/renderer/index.html`.
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { PiniaColada } from '@pinia/colada'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
import i18n from '@memohai/web/i18n'
|
||||
import { setupApiClient } from '@memohai/web/api-client'
|
||||
|
||||
import '@memohai/web/style.css'
|
||||
import 'animate.css'
|
||||
import 'markstream-vue/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
import App from './chat/App.vue'
|
||||
import router from './chat/router'
|
||||
|
||||
setupApiClient({
|
||||
onUnauthorized: () => router.replace({ name: 'Login' }),
|
||||
})
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia().use(piniaPluginPersistedstate))
|
||||
.use(PiniaColada)
|
||||
.use(router)
|
||||
.use(i18n)
|
||||
.mount('#app')
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Settings-window renderer entry. Loaded by the secondary BrowserWindow
|
||||
// created on demand from the chat window. Shares Pinia-persisted state with
|
||||
// the chat window via localStorage. Pairs with `src/renderer/settings.html`.
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { PiniaColada } from '@pinia/colada'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
import i18n from '@memohai/web/i18n'
|
||||
import { setupApiClient } from '@memohai/web/api-client'
|
||||
|
||||
import '@memohai/web/style.css'
|
||||
import 'animate.css'
|
||||
import 'markstream-vue/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
import App from './settings/App.vue'
|
||||
import router from './settings/router'
|
||||
|
||||
setupApiClient({
|
||||
// Settings is a satellite window — it doesn't host the login screen.
|
||||
// On 401 we close ourselves and let the chat window route to login.
|
||||
onUnauthorized: () => {
|
||||
void window.api.window.closeSelf()
|
||||
},
|
||||
})
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia().use(piniaPluginPersistedstate))
|
||||
.use(PiniaColada)
|
||||
.use(router)
|
||||
.use(i18n)
|
||||
.mount('#app')
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { Toaster, SidebarInset } from '@memohai/ui'
|
||||
import 'vue-sonner/style.css'
|
||||
import MainLayout from '@memohai/web/layout/main-layout/index.vue'
|
||||
import SettingsSidebar from '@memohai/web/components/settings-sidebar/index.vue'
|
||||
import { useSettingsStore } from '@memohai/web/store/settings'
|
||||
|
||||
useSettingsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="[&_input]:shadow-none!">
|
||||
<MainLayout>
|
||||
<template #sidebar>
|
||||
<!-- Desktop hosts settings in a dedicated window, so the sidebar's
|
||||
"← Settings" header (back-to-chat affordance) is suppressed. -->
|
||||
<SettingsSidebar :hide-header="true" />
|
||||
</template>
|
||||
<template #main>
|
||||
<SidebarInset class="flex flex-col overflow-hidden">
|
||||
<section class="flex-1 overflow-y-auto">
|
||||
<router-view v-slot="{ Component }">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</router-view>
|
||||
</section>
|
||||
</SidebarInset>
|
||||
</template>
|
||||
</MainLayout>
|
||||
<Toaster position="top-center" />
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
|
||||
// Settings-window router. Mirrors the path layout under `/settings/*` from
|
||||
// @memohai/web's main router so the reused @memohai/web `SettingsSidebar`
|
||||
// (whose `isItemActive` checks `route.path.startsWith('/settings/bots')`)
|
||||
// keeps highlighting the active item correctly. The window boots straight
|
||||
// into `/settings/bots`.
|
||||
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/settings/bots' },
|
||||
{ path: '/settings', redirect: '/settings/bots' },
|
||||
{
|
||||
path: '/settings/bots',
|
||||
name: 'bots',
|
||||
component: () => import('@memohai/web/pages/bots/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/bots/:botId',
|
||||
name: 'bot-detail',
|
||||
component: () => import('@memohai/web/pages/bots/detail.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/providers',
|
||||
name: 'providers',
|
||||
component: () => import('@memohai/web/pages/providers/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/web-search',
|
||||
name: 'web-search',
|
||||
component: () => import('@memohai/web/pages/web-search/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/memory',
|
||||
name: 'memory',
|
||||
component: () => import('@memohai/web/pages/memory/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/speech',
|
||||
name: 'speech',
|
||||
component: () => import('@memohai/web/pages/speech/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/transcription',
|
||||
name: 'transcription',
|
||||
component: () => import('@memohai/web/pages/transcription/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/email',
|
||||
name: 'email',
|
||||
component: () => import('@memohai/web/pages/email/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/browser',
|
||||
name: 'browser',
|
||||
component: () => import('@memohai/web/pages/browser/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/usage',
|
||||
name: 'usage',
|
||||
component: () => import('@memohai/web/pages/usage/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/profile',
|
||||
name: 'profile',
|
||||
component: () => import('@memohai/web/pages/profile/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/platform',
|
||||
name: 'platform',
|
||||
component: () => import('@memohai/web/pages/platform/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/supermarket',
|
||||
name: 'supermarket',
|
||||
component: () => import('@memohai/web/pages/supermarket/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings/about',
|
||||
name: 'about',
|
||||
component: () => import('@memohai/web/pages/about/index.vue'),
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
router.onError((error: Error) => {
|
||||
const isChunkLoadError =
|
||||
error.message.includes('Failed to fetch dynamically imported module') ||
|
||||
error.message.includes('Importing a module script failed') ||
|
||||
error.message.includes('error loading dynamically imported module')
|
||||
if (isChunkLoadError) {
|
||||
console.warn('[Router] Chunk load failed, reloading...', error.message)
|
||||
window.location.reload()
|
||||
return
|
||||
}
|
||||
throw error
|
||||
})
|
||||
|
||||
export default router
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// Minimal type stub for @memohai/ui consumed directly by the desktop
|
||||
// renderer's own files. @memohai/ui ships a barrel `index.ts` that imports
|
||||
// every component; pulling those into desktop's typecheck program surfaces
|
||||
// pre-existing strict-template warnings unrelated to desktop. Routing the
|
||||
// typecheck through this stub keeps the desktop's surface small.
|
||||
//
|
||||
// Vite ignores `paths` and resolves the real `@memohai/ui` package at bundle
|
||||
// time, so runtime behavior is unchanged.
|
||||
|
||||
declare module '@memohai/ui' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
type LooseComponent = DefineComponent<
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>,
|
||||
unknown
|
||||
>
|
||||
|
||||
export const Toaster: LooseComponent
|
||||
export const SidebarInset: LooseComponent
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// Type stubs for @memohai/web subpath imports consumed by the renderer.
|
||||
// We route typechecking through these stubs (via tsconfig `paths`) so vue-tsc
|
||||
// does not recursively typecheck @memohai/web's source tree — @memohai/web
|
||||
// owns its own types/CI. Vite ignores `paths` and resolves the real `exports`
|
||||
// entries at bundle time, so runtime behavior is unchanged.
|
||||
|
||||
// Marker to make this file a module (not an ambient script). Required so
|
||||
// that paths-mapped dynamic `import('@memohai/web/...')` calls succeed —
|
||||
// TS otherwise complains that the resolved file "is not a module".
|
||||
export {}
|
||||
|
||||
declare module '@memohai/web/router' {
|
||||
import type { Router } from 'vue-router'
|
||||
const router: Router
|
||||
export default router
|
||||
}
|
||||
|
||||
declare module '@memohai/web/i18n' {
|
||||
import type { I18n } from 'vue-i18n'
|
||||
const i18n: I18n
|
||||
export default i18n
|
||||
}
|
||||
|
||||
declare module '@memohai/web/api-client' {
|
||||
export interface SetupApiClientOptions {
|
||||
onUnauthorized?: () => void
|
||||
}
|
||||
export function setupApiClient(options?: SetupApiClientOptions): void
|
||||
}
|
||||
|
||||
declare module '@memohai/web/store/settings' {
|
||||
// We don't need the concrete Pinia store type here — desktop just calls the
|
||||
// composable for its registration side-effect.
|
||||
export function useSettingsStore(): unknown
|
||||
}
|
||||
|
||||
declare module '@memohai/web/style.css'
|
||||
|
||||
// Fallback for every Vue SFC reachable through the @memohai/web/* wildcard
|
||||
// export. The TS ambient-module `*` token matches multi-segment paths
|
||||
// (slashes included), so this single declaration covers `pages/.../*.vue`,
|
||||
// `components/.../*.vue`, `layout/.../*.vue`, etc.
|
||||
declare module '@memohai/web/*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>
|
||||
export default component
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": [
|
||||
"src/renderer/src/**/*",
|
||||
"src/renderer/src/**/*.vue",
|
||||
"src/renderer/types/**/*.d.ts",
|
||||
"src/preload/*.d.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["vite/client"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@renderer/*": ["src/renderer/src/*"],
|
||||
"@/*": ["../web/src/*"],
|
||||
"#/*": ["../../packages/ui/src/*"]
|
||||
"@memohai/web/*": ["src/renderer/types/web-stubs.d.ts"],
|
||||
"@memohai/ui": ["src/renderer/types/ui-stubs.d.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
".": "./src/main.ts",
|
||||
"./main": "./src/main.ts",
|
||||
"./App.vue": "./src/App.vue",
|
||||
"./router": "./src/router.ts",
|
||||
"./i18n": "./src/i18n.ts",
|
||||
"./api-client": "./src/lib/api-client.ts",
|
||||
"./style.css": "./src/style.css",
|
||||
"./*": "./src/*"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<aside>
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader class="p-0 border-0">
|
||||
<SidebarHeader
|
||||
v-if="!hideHeader"
|
||||
class="p-0 border-0"
|
||||
>
|
||||
<button
|
||||
class="h-[53px] flex items-center gap-2.5 px-3.5 w-full border-b border-border text-foreground hover:bg-accent/50 transition-colors group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0"
|
||||
@click="router.push(backToChatRoute)"
|
||||
@@ -66,6 +69,15 @@ import {
|
||||
SidebarRail,
|
||||
} from '@memohai/ui'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
// When true, the back-to-chat button in the sidebar header is hidden.
|
||||
// Used by the desktop shell where settings lives in a dedicated window
|
||||
// and "back to chat" is not a meaningful action.
|
||||
hideHeader?: boolean
|
||||
}>(), {
|
||||
hideHeader: false,
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { client } from '@memohai/sdk/client'
|
||||
import router from '@/router'
|
||||
|
||||
export interface SetupApiClientOptions {
|
||||
// Called after the access token is cleared on a 401. Hosts (web / desktop
|
||||
// chat window / desktop settings window) decide what to do — usually a
|
||||
// router redirect to the login screen, but desktop satellite windows may
|
||||
// prefer to close themselves and let the chat window take over auth.
|
||||
onUnauthorized?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the SDK client with base URL, auth interceptor, and 401 handling.
|
||||
* Call this once at app startup (main.ts).
|
||||
*/
|
||||
export function setupApiClient() {
|
||||
export function setupApiClient(options: SetupApiClientOptions = {}) {
|
||||
const apiBaseUrl = import.meta.env.VITE_API_URL?.trim() || '/api'
|
||||
const agentBaseUrl = import.meta.env.VITE_AGENT_URL?.trim() || '/agent'
|
||||
void agentBaseUrl
|
||||
@@ -25,7 +32,7 @@ export function setupApiClient() {
|
||||
client.interceptors.response.use((response) => {
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
router.replace({ name: 'Login' })
|
||||
options.onUnauthorized?.()
|
||||
}
|
||||
return response
|
||||
})
|
||||
|
||||
@@ -4,9 +4,6 @@ import App from './App.vue'
|
||||
import router from './router'
|
||||
import { setupApiClient } from './lib/api-client'
|
||||
import 'animate.css'
|
||||
|
||||
// Configure SDK client before anything else
|
||||
setupApiClient()
|
||||
import { createPinia } from 'pinia'
|
||||
import i18n from './i18n'
|
||||
import { PiniaColada } from '@pinia/colada'
|
||||
@@ -14,6 +11,10 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import 'markstream-vue/index.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
setupApiClient({
|
||||
onUnauthorized: () => router.replace({ name: 'Login' }),
|
||||
})
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia().use(piniaPluginPersistedstate))
|
||||
.use(PiniaColada)
|
||||
|
||||
Generated
+344
@@ -104,6 +104,9 @@ importers:
|
||||
'@memohai/config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/config
|
||||
'@pinia/colada':
|
||||
specifier: ^0.21.2
|
||||
version: 0.21.2(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2(vite@8.0.1(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
@@ -113,6 +116,12 @@ importers:
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^6.0.5
|
||||
version: 6.0.5(vite@8.0.1(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||
'@vue/tsconfig':
|
||||
specifier: ^0.8.1
|
||||
version: 0.8.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
|
||||
animate.css:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
electron:
|
||||
specifier: ^34.5.0
|
||||
version: 34.5.8
|
||||
@@ -122,6 +131,24 @@ importers:
|
||||
electron-vite:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.1(vite@8.0.1(@types/node@24.10.4)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
katex:
|
||||
specifier: ^0.16.37
|
||||
version: 0.16.37
|
||||
markstream-vue:
|
||||
specifier: 0.0.7-beta.2
|
||||
version: 0.0.7-beta.2(katex@0.16.37)(mermaid@11.12.3)(shiki@3.23.0)(stream-markdown@0.0.13(shiki@3.23.0))(stream-monaco@0.0.18(monaco-editor@0.52.2))(vue-i18n@11.2.8(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
|
||||
pinia-plugin-persistedstate:
|
||||
specifier: ^4.7.1
|
||||
version: 4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))
|
||||
png-to-ico:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
typescript:
|
||||
specifier: ~5.9.3
|
||||
version: 5.9.3
|
||||
@@ -131,6 +158,15 @@ importers:
|
||||
vue:
|
||||
specifier: ^3.5.24
|
||||
version: 3.5.26(typescript@5.9.3)
|
||||
vue-i18n:
|
||||
specifier: ^11.2.8
|
||||
version: 11.2.8(vue@3.5.26(typescript@5.9.3))
|
||||
vue-router:
|
||||
specifier: ^4.6.4
|
||||
version: 4.6.4(vue@3.5.26(typescript@5.9.3))
|
||||
vue-sonner:
|
||||
specifier: ^2.0.9
|
||||
version: 2.0.9
|
||||
vue-tsc:
|
||||
specifier: ^3.1.4
|
||||
version: 3.2.2(typescript@5.9.3)
|
||||
@@ -1512,6 +1548,159 @@ packages:
|
||||
'@iconify/utils@3.1.0':
|
||||
resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@internationalized/date@3.10.1':
|
||||
resolution: {integrity: sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA==}
|
||||
|
||||
@@ -2222,6 +2411,9 @@ packages:
|
||||
'@types/node@20.19.39':
|
||||
resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==}
|
||||
|
||||
'@types/node@22.19.17':
|
||||
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
|
||||
|
||||
'@types/node@24.10.4':
|
||||
resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==}
|
||||
|
||||
@@ -4529,10 +4721,19 @@ packages:
|
||||
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
|
||||
engines: {node: '>=10.4.0'}
|
||||
|
||||
png-to-ico@3.0.1:
|
||||
resolution: {integrity: sha512-S8BOAoaGd9gT5uaemQ62arIY3Jzco7Uc7LwUTqRyqJDTsKqOAiyfyN4dSdT0D+Zf8XvgztgpRbM5wnQd7EgYwg==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
pngjs@5.0.0:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
pngjs@7.0.0:
|
||||
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
|
||||
engines: {node: '>=14.19.0'}
|
||||
|
||||
points-on-curve@0.2.0:
|
||||
resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
|
||||
|
||||
@@ -4761,6 +4962,10 @@ packages:
|
||||
set-blocking@2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6599,6 +6804,102 @@ snapshots:
|
||||
'@iconify/types': 2.0.0
|
||||
mlly: 1.8.0
|
||||
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.9.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@internationalized/date@3.10.1':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.18
|
||||
@@ -7283,6 +7584,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@22.19.17':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@24.10.4':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
@@ -9840,8 +10145,16 @@ snapshots:
|
||||
base64-js: 1.5.1
|
||||
xmlbuilder: 15.1.1
|
||||
|
||||
png-to-ico@3.0.1:
|
||||
dependencies:
|
||||
'@types/node': 22.19.17
|
||||
minimist: 1.2.8
|
||||
pngjs: 7.0.0
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
pngjs@7.0.0: {}
|
||||
|
||||
points-on-curve@0.2.0: {}
|
||||
|
||||
points-on-path@0.2.1:
|
||||
@@ -10107,6 +10420,37 @@ snapshots:
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.7.3
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
+2
-1
@@ -7,4 +7,5 @@ onlyBuiltDependencies:
|
||||
- sqlite3
|
||||
- electron
|
||||
- electron-winstaller
|
||||
- esbuild
|
||||
- esbuild
|
||||
- sharp
|
||||
Reference in New Issue
Block a user