diff --git a/.gitignore b/.gitignore index d5caf6e..ed9cb68 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ apps/mobile/android/app/src/main/assets/public/ apps/mobile/android/capacitor-cordova-android-plugins/ apps/mobile/ios/App/App/public/ apps/mobile/ios/DerivedData/ +apps/desktop/release/ +apps/desktop/native/bin/ diff --git a/README.md b/README.md index f3de66c..9352d90 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Read your sources, ask with citations, write and learn — in one workspace you [![CI](https://img.shields.io/github/actions/workflow/status/Mar7thLover/QuireNote/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/Mar7thLover/QuireNote/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-C06240?style=flat-square)](LICENSE) [![Node](https://img.shields.io/badge/node-%E2%89%A522.12-5A5A55?style=flat-square)](https://nodejs.org) -[![Platforms](https://img.shields.io/badge/platforms-web%20%C2%B7%20iOS%20%C2%B7%20Android-5A5A55?style=flat-square)](docs/mobile.md) +[![Platforms](https://img.shields.io/badge/platforms-web%20%C2%B7%20macOS%20%C2%B7%20iOS%20%C2%B7%20Android-5A5A55?style=flat-square)](docs/mobile.md) [![Docker](https://img.shields.io/badge/deploy-Docker-5A5A55?style=flat-square)](#docker) **English** · [简体中文](README.zh-CN.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 2838f21..314b664 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -10,7 +10,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/Mar7thLover/QuireNote/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/Mar7thLover/QuireNote/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-C06240?style=flat-square)](LICENSE) [![Node](https://img.shields.io/badge/node-%E2%89%A522.12-5A5A55?style=flat-square)](https://nodejs.org) -[![Platforms](https://img.shields.io/badge/platforms-web%20%C2%B7%20iOS%20%C2%B7%20Android-5A5A55?style=flat-square)](docs/mobile.md) +[![Platforms](https://img.shields.io/badge/platforms-web%20%C2%B7%20macOS%20%C2%B7%20iOS%20%C2%B7%20Android-5A5A55?style=flat-square)](docs/mobile.md) [![Docker](https://img.shields.io/badge/deploy-Docker-5A5A55?style=flat-square)](#docker-一键部署) [English](README.md) · **简体中文** diff --git a/apps/desktop/build/icon.png b/apps/desktop/build/icon.png new file mode 100644 index 0000000..f7e67f3 Binary files /dev/null and b/apps/desktop/build/icon.png differ diff --git a/apps/desktop/electron-builder.yml b/apps/desktop/electron-builder.yml new file mode 100644 index 0000000..cdf6f2b --- /dev/null +++ b/apps/desktop/electron-builder.yml @@ -0,0 +1,18 @@ +appId: app.quirenote.notebook +productName: QuireNote +asar: false +directories: + output: release +files: + - src/main.mjs + - package.json + - from: dist + to: server +mac: + category: public.app-category.productivity + icon: build/icon.png + identity: null + target: + - dir + - zip +npmRebuild: false diff --git a/apps/desktop/native/package-lock.json b/apps/desktop/native/package-lock.json new file mode 100644 index 0000000..020c4d2 --- /dev/null +++ b/apps/desktop/native/package-lock.json @@ -0,0 +1,113 @@ +{ + "name": "quirenote-desktop-natives", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quirenote-desktop-natives", + "dependencies": { + "better-sqlite3": "13.0.3", + "sqlite-vec": "0.1.9", + "sqlite-vec-darwin-arm64": "0.1.9" + } + }, + "node_modules/better-sqlite3": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", + "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/sqlite-vec": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.9.tgz", + "integrity": "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==", + "license": "MIT OR Apache", + "optionalDependencies": { + "sqlite-vec-darwin-arm64": "0.1.9", + "sqlite-vec-darwin-x64": "0.1.9", + "sqlite-vec-linux-arm64": "0.1.9", + "sqlite-vec-linux-x64": "0.1.9", + "sqlite-vec-windows-x64": "0.1.9" + } + }, + "node_modules/sqlite-vec-darwin-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.9.tgz", + "integrity": "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-darwin-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.9.tgz", + "integrity": "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-linux-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.9.tgz", + "integrity": "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-linux-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.9.tgz", + "integrity": "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-windows-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.9.tgz", + "integrity": "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "win32" + ] + } + } +} diff --git a/apps/desktop/native/package.json b/apps/desktop/native/package.json new file mode 100644 index 0000000..b4b744a --- /dev/null +++ b/apps/desktop/native/package.json @@ -0,0 +1,9 @@ +{ + "name": "quirenote-desktop-natives", + "private": true, + "dependencies": { + "better-sqlite3": "13.0.3", + "sqlite-vec": "0.1.9", + "sqlite-vec-darwin-arm64": "0.1.9" + } +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000..8b2ced5 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,21 @@ +{ + "name": "@quirenote/desktop", + "version": "0.0.0", + "private": true, + "description": "QuireNote desktop app (macOS): Electron shell running the embedded server and web build", + "main": "src/main.mjs", + "scripts": { + "bundle": "node scripts/bundle.mjs", + "natives": "node scripts/natives.mjs", + "setup": "pnpm natives && pnpm bundle", + "dev": "pnpm setup && electron .", + "pack": "pnpm setup && electron-builder --dir", + "dist": "pnpm setup && electron-builder --mac zip" + }, + "devDependencies": { + "@electron/rebuild": "^4.0.1", + "electron": "^44.9.0", + "electron-builder": "^26.15.3", + "esbuild": "^0.25.10" + } +} diff --git a/apps/desktop/scripts/bundle.mjs b/apps/desktop/scripts/bundle.mjs new file mode 100644 index 0000000..6af979f --- /dev/null +++ b/apps/desktop/scripts/bundle.mjs @@ -0,0 +1,53 @@ +/** + * 打包内嵌资源:esbuild 把服务端(workspace TS 源码)打成单文件, + * 复制 Web 构建产物与原生依赖到 dist/。结构: + * dist/server.mjs 服务端单文件(better-sqlite3 的 JS 也打进 bundle; + * .node / .dylib 由外壳通过 QUIREENOTE_* env 指定路径) + * dist/bin/* Electron ABI 的原生二进制(由 scripts/natives.mjs 生成) + * dist/web/* apps/web 构建产物 + */ +import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { build } from "esbuild"; + +const desktopDir = resolve(dirname(new URL(import.meta.url).pathname), ".."); +const repoRoot = resolve(desktopDir, "../.."); +const outDir = join(desktopDir, "dist"); +const nativesBin = join(desktopDir, "native/bin"); +const webDist = join(repoRoot, "apps/web/dist"); + +if (!existsSync(join(webDist, "index.html"))) { + console.error("[bundle] 缺少 apps/web/dist,先运行 pnpm --filter @quirenote/web build"); + process.exit(1); +} +if (!existsSync(join(nativesBin, "better_sqlite3.node"))) { + console.error("[bundle] 缺少 native/bin,先运行 pnpm --filter @quirenote/desktop natives"); + process.exit(1); +} + +rmSync(outDir, { recursive: true, force: true }); +mkdirSync(outDir, { recursive: true }); + +await build({ + entryPoints: [join(desktopDir, "src/server-entry.mjs")], + bundle: true, + format: "esm", + platform: "node", + target: "node22", + outfile: join(outDir, "server.mjs"), + // better-sqlite3 / sqlite-vec 的 JS 一并打包;.node / .dylib 二进制不进 bundle, + // 运行时由 QUIREENOTE_SQLITE3_BINDING / QUIREENOTE_SQLITE_VEC_EXT 指定。 + external: ["*.node", "*.dylib"], + // 服务端的依赖(hono、better-sqlite3 等)在 apps/server/node_modules 下解析 + nodePaths: [join(repoRoot, "apps/server/node_modules"), join(repoRoot, "node_modules")], + banner: { + // CJS 依赖里的 require()(含 node builtin)在 ESM 产物中需要真实 require; + // 用别名避开 esbuild 在打包产物里自动注入的同名 import。 + js: 'import { createRequire as __cr } from "node:module";\nconst require = __cr(import.meta.url);', + }, +}); + +cpSync(webDist, join(outDir, "web"), { recursive: true }); +cpSync(nativesBin, join(outDir, "bin"), { recursive: true }); + +console.log(`[bundle] server + web + natives → ${outDir}`); diff --git a/apps/desktop/scripts/natives.mjs b/apps/desktop/scripts/natives.mjs new file mode 100644 index 0000000..cef11aa --- /dev/null +++ b/apps/desktop/scripts/natives.mjs @@ -0,0 +1,45 @@ +/** + * 准备 Electron ABI 的原生模块:在 apps/desktop/native/ 下独立安装 + * better-sqlite3 / sqlite-vec,并用 @electron/rebuild 针对 Electron 重编译。 + * 独立目录是为了不污染 workspace 里按系统 Node ABI 构建的同名包。 + */ +import { execSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join, resolve } from "node:path"; +import { rebuild } from "@electron/rebuild"; + +const desktopDir = resolve(dirname(new URL(import.meta.url).pathname), ".."); +const nativeDir = join(desktopDir, "native"); +const require = createRequire(import.meta.url); + +const electronVersion = JSON.parse( + readFileSync(require.resolve("electron/package.json", { paths: [desktopDir] }), "utf8"), +).version; + +if (!existsSync(join(nativeDir, "node_modules/better-sqlite3"))) { + console.log(`[natives] 安装原生依赖(better-sqlite3 / sqlite-vec)…`); + execSync("npm install --omit=dev --no-audit --no-fund", { cwd: nativeDir, stdio: "inherit" }); +} + +console.log(`[natives] 针对 Electron ${electronVersion} 重编译 better-sqlite3…`); +await rebuild({ + buildPath: nativeDir, + electronVersion, + arch: process.arch, + force: true, + onlyModules: ["better-sqlite3"], +}); + +// 只导出运行时真正需要的两个二进制,打包时随 dist/bin 进 .app(不进 node_modules) +const binDir = join(nativeDir, "bin"); +mkdirSync(binDir, { recursive: true }); +copyFileSync( + join(nativeDir, `node_modules/better-sqlite3/prebuilds/${process.platform}-${process.arch}.node`), + join(binDir, "better_sqlite3.node"), +); +copyFileSync( + join(nativeDir, `node_modules/sqlite-vec-${process.platform}-${process.arch}/vec0.dylib`), + join(binDir, "vec0.dylib"), +); +console.log(`[natives] 完成,二进制在 ${binDir}`); diff --git a/apps/desktop/src/main.mjs b/apps/desktop/src/main.mjs new file mode 100644 index 0000000..c9b5929 --- /dev/null +++ b/apps/desktop/src/main.mjs @@ -0,0 +1,102 @@ +/** + * QuireNote 桌面端(macOS)主进程:内嵌服务端 + 内置 Web 界面。 + * 数据保存在 userData/data,与自托管部署同构但开箱即用。 + */ +import { createServer } from "node:net"; +import { dirname, join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { app, BrowserWindow, dialog, shell } from "electron"; + +app.setName("QuireNote"); + +const here = dirname(new URL(import.meta.url).pathname); +// 打包后资源在 resources/app/server/(files 配置),开发态在 apps/desktop/dist/ +const resourceBase = app.isPackaged ? join(process.resourcesPath, "app", "server") : join(here, "..", "dist"); +const serverBundle = join(resourceBase, "server.mjs"); +const webDist = join(resourceBase, "web"); +const binDir = join(resourceBase, "bin"); + +let closing = false; +let closeServer = async () => {}; + +function freePort() { + return new Promise((resolve, reject) => { + const s = createServer(); + s.once("error", reject); + s.listen(0, "127.0.0.1", () => { + const { port } = s.address(); + s.close(() => resolve(port)); + }); + }); +} + +async function startServer() { + const port = await freePort(); + process.env.PORT = String(port); + process.env.QUIRENOTE_DATA_DIR = join(app.getPath("userData"), "data"); + process.env.QUIRENOTE_WEB_DIST = webDist; + process.env.QUIRENOTE_SQLITE3_BINDING = join(binDir, "better_sqlite3.node"); + process.env.QUIRENOTE_SQLITE_VEC_EXT = join(binDir, "vec0.dylib"); + // 桌面端默认关闭种子数据可通过 QUIREENOTE_SEED=0;保持默认与 Docker 一致。 + const mod = await import(pathToFileURL(serverBundle).href); + closeServer = mod.close; + return port; +} + +async function waitForHealth(port, timeoutMs = 30000) { + const url = `http://127.0.0.1:${port}/api/health`; + const deadline = Date.now() + timeoutMs; + let lastErr; + while (Date.now() < deadline) { + try { + const res = await fetch(url); + if (res.ok) return; + lastErr = new Error(`health ${res.status}`); + } catch (e) { + lastErr = e; + } + await new Promise((r) => setTimeout(r, 200)); + } + throw lastErr ?? new Error("server did not become healthy"); +} + +async function createMainWindow(port) { + const win = new BrowserWindow({ + width: 1440, + height: 960, + minWidth: 960, + minHeight: 600, + title: "QuireNote", + backgroundColor: "#141311", + webPreferences: { contextIsolation: true, nodeIntegration: false }, + }); + // 外链交给系统浏览器,应用内只导航本机地址 + win.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: "deny" }; + }); + await win.loadURL(`http://127.0.0.1:${port}`); +} + +app.whenReady().then(async () => { + try { + const port = await startServer(); + await waitForHealth(port); + await createMainWindow(port); + } catch (err) { + console.error(err); + dialog.showErrorBox("QuireNote 启动失败", String(err?.stack ?? err)); + app.exit(1); + } +}); + +app.on("window-all-closed", () => app.quit()); + +app.on("before-quit", (e) => { + if (closing) return; + e.preventDefault(); + closing = true; + closeServer() + .catch(() => {}) + .finally(() => app.quit()); +}); diff --git a/apps/desktop/src/server-entry.mjs b/apps/desktop/src/server-entry.mjs new file mode 100644 index 0000000..6949dda --- /dev/null +++ b/apps/desktop/src/server-entry.mjs @@ -0,0 +1,26 @@ +/** + * 桌面端内嵌服务端入口:与 apps/server/src/main.ts 等价,但把 HTTP 句柄暴露给 + * Electron 主进程,退出时优雅关闭。打包时由 scripts/bundle.mjs 用 esbuild 打成单文件。 + */ +import { serve } from "@hono/node-server"; +import { boot } from "../../server/src/boot"; +import { loadConfig, VERSION } from "../../server/src/config"; +import { bootLang, setDefaultLang } from "../../server/src/lang"; + +setDefaultLang(bootLang(process.env)); +const config = loadConfig(); +const log = (msg) => console.log(msg); + +const server = await boot(config, { env: process.env, log }); + +// 桌面版只监听回环地址:数据属于本机,不暴露到局域网。 +const http = serve({ fetch: server.app.fetch, port: config.port, hostname: "127.0.0.1" }, (info) => { + log( + `[quirenote] desktop server ${VERSION} listening on http://127.0.0.1:${info.port} (data: ${config.dataDir})`, + ); +}); + +export async function close() { + http.close(); + await server.close(); +} diff --git a/apps/mobile/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/mobile/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..5ad0dc2 --- /dev/null +++ b/apps/mobile/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "77e3744b0f953e10a9d022c521f61508ebcec1cd5ea02d25065dd0c1a0db9046", + "pins" : [ + { + "identity" : "capacitor-swift-pm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ionic-team/capacitor-swift-pm.git", + "state" : { + "revision" : "4c7f346d16196e21fbe23d4a7a6fc7af62af6742", + "version" : "8.5.3" + } + } + ], + "version" : 3 +} diff --git a/apps/mobile/ios/App/App/Info.plist b/apps/mobile/ios/App/App/Info.plist index 2ef3acd..caafee7 100644 --- a/apps/mobile/ios/App/App/Info.plist +++ b/apps/mobile/ios/App/App/Info.plist @@ -77,5 +77,10 @@ UIViewControllerBasedStatusBarAppearance + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 76e7f98..f6e9065 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -86,6 +86,14 @@ export function createApp( await next(); if (c.res.ok) c.header("Cache-Control", "public, max-age=31536000, immutable"); }); + // index.html 不能长缓存:它引用带 hash 的 assets,升级后旧入口会指向已删除的 chunk, + // 导致动态导入失败白屏(Electron webview 的磁盘缓存尤其明显)。 + app.use("*", async (c, next) => { + await next(); + const ct = c.res.headers.get("content-type") ?? ""; + if (ct.includes("text/html") && !c.res.headers.get("cache-control")) + c.header("Cache-Control", "no-cache"); + }); // 分享链接的只读页:标题和介绍写进 meta(聊天软件贴链接时显示),不进搜索引擎、不带 Referer app.get("/share/:token", async (c) => { const meta = await shareMeta(ctx, c.req.param("token")); @@ -95,7 +103,9 @@ export function createApp( return c.html(shareHtml(indexHtml, meta), meta ? 200 : 404); }); app.use("*", serveStatic({ root: dist })); - app.get("*", (c) => (c.req.path.startsWith("/api/") ? c.notFound() : c.html(indexHtml))); + app.get("*", (c) => + c.req.path.startsWith("/api/") || c.req.path.startsWith("/assets/") ? c.notFound() : c.html(indexHtml), + ); } else { app.get("/", (c) => c.json({ name: "quirenote-server", api: "/api", health: "/api/health" })); } diff --git a/apps/server/src/store/sqlite.ts b/apps/server/src/store/sqlite.ts index 34b36e3..1efe040 100644 --- a/apps/server/src/store/sqlite.ts +++ b/apps/server/src/store/sqlite.ts @@ -173,13 +173,28 @@ function migrate(db: DB): void { } } +/** + * 桌面端打包时原生二进制没有 node_modules 可解析,由外壳通过 + * QUIREENOTE_SQLITE3_BINDING / QUIREENOTE_SQLITE_VEC_EXT 直接给出文件路径。 + */ +function openDatabase(path: string | Buffer): DB { + const binding = process.env.QUIRENOTE_SQLITE3_BINDING?.trim(); + return binding ? new Database(path, { nativeBinding: binding }) : new Database(path); +} + +function loadVec(db: DB): void { + const ext = process.env.QUIRENOTE_SQLITE_VEC_EXT?.trim(); + if (ext) db.loadExtension(ext); + else sqliteVec.load(db); +} + function openDb(path: string): DB { if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true }); - const db = new Database(path); + const db = openDatabase(path); db.pragma("journal_mode = WAL"); db.pragma("busy_timeout = 5000"); db.pragma("synchronous = NORMAL"); - sqliteVec.load(db); + loadVec(db); migrate(db); return db; } @@ -884,7 +899,7 @@ export class SqliteStore implements Store { async restore(bytes: Uint8Array): Promise { // 先校验快照可以打开,并且是 QuireNote 的库。 - const probe = new Database(inMemoryImage(bytes)); + const probe = openDatabase(inMemoryImage(bytes)); try { probe.prepare("SELECT COUNT(*) FROM notebooks").get(); } catch { @@ -895,8 +910,8 @@ export class SqliteStore implements Store { this.db.close(); this.vecTables.clear(); if (this.path === ":memory:") { - this.db = new Database(inMemoryImage(bytes)); - sqliteVec.load(this.db); + this.db = openDatabase(inMemoryImage(bytes)); + loadVec(this.db); migrate(this.db); return; } diff --git a/apps/web/src/components/AppSidebar.tsx b/apps/web/src/components/AppSidebar.tsx index ef312ec..f5d6efc 100644 --- a/apps/web/src/components/AppSidebar.tsx +++ b/apps/web/src/components/AppSidebar.tsx @@ -1,5 +1,5 @@ import type { Notebook } from "@quirenote/core"; -import { cn, Kbd, Tooltip } from "@quirenote/ui"; +import { cn, Shortcut, Tooltip } from "@quirenote/ui"; import { useQuery } from "@tanstack/react-query"; import { Link, useNavigate, useRouterState } from "@tanstack/react-router"; import { @@ -29,6 +29,7 @@ import { type ReactNode, useEffect } from "react"; import { currentLang, setLang, useTranslation } from "@/i18n"; import { api } from "@/lib/api"; import { contextMenu } from "@/lib/contextMenu"; +import { SHORTCUTS } from "@/lib/hotkeys"; import { qk } from "@/lib/query"; import { resolvedMode, useTheme } from "@/stores/theme"; import { useUi } from "@/stores/ui"; @@ -260,10 +261,7 @@ export function AppSidebar({ > {t("nav.searchCommand")} - - Ctrl - K - + )} diff --git a/apps/web/src/features/editor/embeds.tsx b/apps/web/src/features/editor/embeds.tsx index 842050d..e5bd143 100644 --- a/apps/web/src/features/editor/embeds.tsx +++ b/apps/web/src/features/editor/embeds.tsx @@ -16,6 +16,7 @@ import { useCallback, useMemo, useState } from "react"; import { autoHeight, useCardThemeVars } from "@/features/render/RenderedSegments"; import i18n, { useTranslation } from "@/i18n"; import { api, apiBase } from "@/lib/api"; +import { modKey } from "@/lib/hotkeys"; import { videoEmbedUrl } from "@/lib/video"; /** asset://id → 服务端地址;其他地址原样。 */ @@ -194,7 +195,7 @@ function HtmlBlockView({ node, updateAttributes, selected, editor }: NodeViewPro
- {t("editor.htmlEditing")} + {t("editor.htmlEditing", { mod: modKey })}