mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat: store local data to local db (#2037)
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
cacheFolder: '../../.yarn/cache'
|
||||
deferredVersionFolder: '../../.yarn/versions'
|
||||
globalFolder: '../../.yarn/global'
|
||||
installStatePath: '../../.yarn/install-state.gz'
|
||||
patchFolder: '../../.yarn/patches'
|
||||
pnpUnpluggedFolder: '../../.yarn/unplugged'
|
||||
yarnPath: '../../.yarn/releases/yarn-3.5.0.cjs'
|
||||
virtualFolder: '../../.yarn/__virtual__'
|
||||
@@ -3,8 +3,10 @@ const {
|
||||
utils: { fromBuildIdentifier },
|
||||
} = require('@electron-forge/core');
|
||||
|
||||
const isCanary = process.env.BUILD_TYPE === 'canary';
|
||||
const path = require('node:path');
|
||||
|
||||
const isCanary = process.env.BUILD_TYPE === 'canary';
|
||||
const buildType = isCanary ? 'canary' : 'stable';
|
||||
const productName = isCanary ? 'AFFiNE-Canary' : 'AFFiNE';
|
||||
const icoPath = isCanary
|
||||
? './resources/icons/icon_canary.ico'
|
||||
@@ -13,6 +15,11 @@ const icnsPath = isCanary
|
||||
? './resources/icons/icon_canary.icns'
|
||||
: './resources/icons/icon.icns';
|
||||
|
||||
const arch =
|
||||
process.argv.indexOf('--arch') > 0
|
||||
? process.argv[process.argv.indexOf('--arch') + 1]
|
||||
: process.arch;
|
||||
|
||||
/**
|
||||
* @type {import('@electron-forge/shared-types').ForgeConfig}
|
||||
*/
|
||||
@@ -25,10 +32,10 @@ module.exports = {
|
||||
stable: 'pro.affine.app',
|
||||
}),
|
||||
icon: icnsPath,
|
||||
osxSign: {
|
||||
identity: 'Developer ID Application: TOEVERYTHING PTE. LTD.',
|
||||
'hardened-runtime': true,
|
||||
},
|
||||
// osxSign: {
|
||||
// identity: 'Developer ID Application: TOEVERYTHING PTE. LTD.',
|
||||
// 'hardened-runtime': true,
|
||||
// },
|
||||
osxNotarize: process.env.APPLE_ID
|
||||
? {
|
||||
tool: 'notarytool',
|
||||
@@ -42,9 +49,25 @@ module.exports = {
|
||||
{
|
||||
name: '@electron-forge/maker-dmg',
|
||||
config: {
|
||||
format: 'ULFO',
|
||||
icon: icnsPath,
|
||||
name: 'AFFiNE',
|
||||
'icon-size': 128,
|
||||
background: './resources/icons/dmg-background.png',
|
||||
contents: [
|
||||
{
|
||||
x: 176,
|
||||
y: 192,
|
||||
type: 'file',
|
||||
path: path.resolve(
|
||||
__dirname,
|
||||
'out',
|
||||
buildType,
|
||||
`${productName}-darwin-${arch}`,
|
||||
`${productName}.app`
|
||||
),
|
||||
},
|
||||
{ x: 432, y: 192, type: 'link', path: '/Applications' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
3
apps/electron/layers/logger.ts
Normal file
3
apps/electron/layers/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import log from 'electron-log';
|
||||
|
||||
export const logger = log;
|
||||
@@ -1,69 +0,0 @@
|
||||
import * as os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { app, shell } from 'electron';
|
||||
import { BrowserWindow, ipcMain, nativeTheme } from 'electron';
|
||||
import fs from 'fs-extra';
|
||||
import { parse } from 'url';
|
||||
|
||||
import { isMacOS } from '../../../utils';
|
||||
import { getExchangeTokenParams, oauthEndpoint } from './google-auth';
|
||||
|
||||
const AFFINE_ROOT = path.join(os.homedir(), '.affine');
|
||||
|
||||
fs.ensureDirSync(AFFINE_ROOT);
|
||||
|
||||
const logger = console;
|
||||
|
||||
export const registerHandlers = () => {
|
||||
ipcMain.handle('ui:theme-change', async (_, theme) => {
|
||||
nativeTheme.themeSource = theme;
|
||||
logger.info('theme change', theme);
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:sidebar-visibility-change', async (_, visible) => {
|
||||
// todo
|
||||
// detect if os is macos
|
||||
if (isMacOS()) {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach(w => {
|
||||
// hide window buttons when sidebar is not visible
|
||||
w.setWindowButtonVisibility(visible);
|
||||
});
|
||||
logger.info('sidebar visibility change', visible);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:get-google-oauth-code', async () => {
|
||||
logger.info('starting google sign in ...');
|
||||
shell.openExternal(oauthEndpoint);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleOpenUrl = async (_: any, url: string) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows().find(
|
||||
w => !w.isDestroyed()
|
||||
);
|
||||
const urlObj = parse(url.replace('??', '?'), true);
|
||||
if (!mainWindow || !url.startsWith('affine://auth-callback')) return;
|
||||
const code = urlObj.query['code'] as string;
|
||||
if (!code) return;
|
||||
|
||||
logger.info('google sign in code received from callback', code);
|
||||
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
resolve(getExchangeTokenParams(code));
|
||||
};
|
||||
|
||||
app.on('open-url', handleOpenUrl);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timed out'));
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
}, 30000);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('main:env-update', async (_, env, value) => {
|
||||
process.env[env] = value;
|
||||
});
|
||||
};
|
||||
9
apps/electron/layers/main/src/context.ts
Normal file
9
apps/electron/layers/main/src/context.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { app } from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
export const appContext = {
|
||||
appName: app.name,
|
||||
appDataPath: path.join(app.getPath('appData'), app.name),
|
||||
};
|
||||
|
||||
export type AppContext = typeof appContext;
|
||||
34
apps/electron/layers/main/src/data/export.ts
Normal file
34
apps/electron/layers/main/src/data/export.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { logger } from '../../../logger';
|
||||
import type { WorkspaceDatabase } from './sqlite';
|
||||
|
||||
/**
|
||||
* Start a backup of the database to the given destination.
|
||||
*/
|
||||
export async function exportDatabase(db: WorkspaceDatabase, dest: string) {
|
||||
await fs.copyFile(db.path, dest);
|
||||
logger.log('export: ', dest);
|
||||
}
|
||||
|
||||
// export async function startBackup(db: WorkspaceDatabase, dest: string) {
|
||||
// let timeout: NodeJS.Timeout | null;
|
||||
// async function backup() {
|
||||
// await fs.copyFile(db.path, dest);
|
||||
// logger.log('backup: ', dest);
|
||||
// }
|
||||
|
||||
// backup();
|
||||
|
||||
// const _db = await db.sqliteDB$;
|
||||
|
||||
// _db.on('change', () => {
|
||||
// if (timeout) {
|
||||
// clearTimeout(timeout);
|
||||
// }
|
||||
// timeout = setTimeout(async () => {
|
||||
// await backup();
|
||||
// timeout = null;
|
||||
// }, 1000);
|
||||
// });
|
||||
// }
|
||||
216
apps/electron/layers/main/src/data/sqlite.ts
Normal file
216
apps/electron/layers/main/src/data/sqlite.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import type { Database } from 'sqlite3';
|
||||
import sqlite3Default from 'sqlite3';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { logger } from '../../../logger';
|
||||
import type { AppContext } from '../context';
|
||||
|
||||
const sqlite3 = sqlite3Default.verbose();
|
||||
|
||||
const schemas = [
|
||||
`CREATE TABLE IF NOT EXISTS "updates" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
data BLOB NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS "blobs" (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
data BLOB NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)`,
|
||||
];
|
||||
|
||||
sqlite3.verbose();
|
||||
|
||||
interface UpdateRow {
|
||||
id: number;
|
||||
data: Buffer;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface BlobRow {
|
||||
key: string;
|
||||
data: Buffer;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export class WorkspaceDatabase {
|
||||
sqliteDB$: Promise<Database>;
|
||||
ydoc = new Y.Doc();
|
||||
_db: Database | null = null;
|
||||
|
||||
ready: Promise<Uint8Array>;
|
||||
|
||||
constructor(public path: string) {
|
||||
this.sqliteDB$ = this.reconnectDB();
|
||||
logger.log('open db', path);
|
||||
|
||||
this.ydoc.on('update', update => {
|
||||
this.addUpdateToSQLite(update);
|
||||
});
|
||||
|
||||
this.ready = (async () => {
|
||||
const updates = await this.getUpdates();
|
||||
updates.forEach(update => {
|
||||
Y.applyUpdate(this.ydoc, update.data);
|
||||
});
|
||||
return this.getEncodedDocUpdates();
|
||||
})();
|
||||
}
|
||||
|
||||
// release resources
|
||||
destroy = () => {
|
||||
this._db?.close();
|
||||
this.ydoc.destroy();
|
||||
};
|
||||
|
||||
reconnectDB = async () => {
|
||||
logger.log('open db', this.path);
|
||||
if (this._db) {
|
||||
const _db = this._db;
|
||||
await new Promise<void>(res =>
|
||||
_db.close(() => {
|
||||
res();
|
||||
})
|
||||
);
|
||||
}
|
||||
return (this.sqliteDB$ = new Promise(res => {
|
||||
// use cached version?
|
||||
const db = new sqlite3.Database(this.path, error => {
|
||||
if (error) {
|
||||
logger.error('open db error', error);
|
||||
}
|
||||
});
|
||||
this._db = db;
|
||||
|
||||
db.exec(schemas.join(';'), () => {
|
||||
res(db);
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
getEncodedDocUpdates = () => {
|
||||
return Y.encodeStateAsUpdate(this.ydoc);
|
||||
};
|
||||
|
||||
// non-blocking and use yDoc to validate the update
|
||||
// after that, the update is added to the db
|
||||
applyUpdate = (data: Uint8Array) => {
|
||||
Y.applyUpdate(this.ydoc, data);
|
||||
// todo: trim the updates when the number of records is too large
|
||||
// 1. store the current ydoc state in the db
|
||||
// 2. then delete the old updates
|
||||
// yjs-idb will always trim the db for the first time after DB is loaded
|
||||
};
|
||||
|
||||
addBlob = async (key: string, data: Uint8Array) => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO blobs (key, data) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET data = ?',
|
||||
[key, data, data],
|
||||
err => {
|
||||
if (err) {
|
||||
logger.error('addBlob', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
getBlob = async (key: string) => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<Uint8Array | null>((resolve, reject) => {
|
||||
db.get<BlobRow>(
|
||||
'SELECT data FROM blobs WHERE key = ?',
|
||||
[key],
|
||||
(err, row) => {
|
||||
if (err) {
|
||||
logger.error('getBlob', err);
|
||||
reject(err);
|
||||
} else if (!row) {
|
||||
logger.error('getBlob', 'not found');
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(row.data);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
deleteBlob = async (key: string) => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
db.run('DELETE FROM blobs WHERE key = ?', [key], err => {
|
||||
if (err) {
|
||||
logger.error('deleteBlob', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getPersistentBlobKeys = async () => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
db.all<BlobRow>('SELECT key FROM blobs', (err, rows) => {
|
||||
if (err) {
|
||||
logger.error('getPersistentBlobKeys', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows.map(row => row.key));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private getUpdates = async () => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<{ id: number; data: any }[]>((resolve, reject) => {
|
||||
// do we need to order by id?
|
||||
db.all<UpdateRow>('SELECT * FROM updates', (err, rows) => {
|
||||
if (err) {
|
||||
logger.error('getUpdates', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private addUpdateToSQLite = async (data: Uint8Array) => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
db.run('INSERT INTO updates (data) VALUES (?)', [data], err => {
|
||||
if (err) {
|
||||
logger.error('addUpdateToSQLite', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export async function openWorkspaceDatabase(
|
||||
context: AppContext,
|
||||
workspaceId: string
|
||||
) {
|
||||
const basePath = path.join(context.appDataPath, 'workspaces', workspaceId);
|
||||
// hmmm.... blocking api but it should be fine, right?
|
||||
await fs.ensureDir(basePath);
|
||||
const dbPath = path.join(basePath, 'storage.db');
|
||||
|
||||
return new WorkspaceDatabase(dbPath);
|
||||
}
|
||||
20
apps/electron/layers/main/src/data/workspace.ts
Normal file
20
apps/electron/layers/main/src/data/workspace.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import type { AppContext } from '../context';
|
||||
|
||||
export async function listWorkspaces(context: AppContext) {
|
||||
const basePath = path.join(context.appDataPath, 'workspaces');
|
||||
return fs.readdir(basePath);
|
||||
}
|
||||
|
||||
export async function deleteWorkspace(context: AppContext, id: string) {
|
||||
const basePath = path.join(context.appDataPath, 'workspaces', id);
|
||||
const movedPath = path.join(
|
||||
context.appDataPath,
|
||||
'delete-workspaces',
|
||||
`${id}`
|
||||
);
|
||||
return fs.move(basePath, movedPath);
|
||||
}
|
||||
184
apps/electron/layers/main/src/handlers.ts
Normal file
184
apps/electron/layers/main/src/handlers.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
ipcMain,
|
||||
nativeTheme,
|
||||
shell,
|
||||
} from 'electron';
|
||||
import { parse } from 'url';
|
||||
|
||||
import { logger } from '../../logger';
|
||||
import { isMacOS } from '../../utils';
|
||||
import { appContext } from './context';
|
||||
import { exportDatabase } from './data/export';
|
||||
import type { WorkspaceDatabase } from './data/sqlite';
|
||||
import { openWorkspaceDatabase } from './data/sqlite';
|
||||
import { deleteWorkspace, listWorkspaces } from './data/workspace';
|
||||
import { getExchangeTokenParams, oauthEndpoint } from './google-auth';
|
||||
|
||||
let currentWorkspaceId = '';
|
||||
|
||||
const dbMapping = new Map<string, WorkspaceDatabase>();
|
||||
|
||||
async function ensureWorkspaceDB(id: string) {
|
||||
let workspaceDB = dbMapping.get(id);
|
||||
if (!workspaceDB) {
|
||||
// hmm... potential race condition?
|
||||
workspaceDB = await openWorkspaceDatabase(appContext, id);
|
||||
dbMapping.set(id, workspaceDB);
|
||||
}
|
||||
await workspaceDB.ready;
|
||||
return workspaceDB;
|
||||
}
|
||||
|
||||
function registerWorkspaceHandlers() {
|
||||
ipcMain.handle('workspace:list', async _ => {
|
||||
logger.info('list workspaces');
|
||||
return listWorkspaces(appContext);
|
||||
});
|
||||
|
||||
ipcMain.handle('workspace:delete', async (_, id) => {
|
||||
logger.info('delete workspace', id);
|
||||
return deleteWorkspace(appContext, id);
|
||||
});
|
||||
}
|
||||
|
||||
function registerUIHandlers() {
|
||||
ipcMain.handle('ui:theme-change', async (_, theme) => {
|
||||
nativeTheme.themeSource = theme;
|
||||
logger.info('theme change', theme);
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:sidebar-visibility-change', async (_, visible) => {
|
||||
// todo
|
||||
// detect if os is macos
|
||||
if (isMacOS()) {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach(w => {
|
||||
// hide window buttons when sidebar is not visible
|
||||
w.setWindowButtonVisibility(visible);
|
||||
});
|
||||
logger.info('sidebar visibility change', visible);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:workspace-change', async (_, workspaceId) => {
|
||||
logger.info('workspace change', workspaceId);
|
||||
currentWorkspaceId = workspaceId;
|
||||
});
|
||||
|
||||
// @deprecated
|
||||
ipcMain.handle('ui:get-google-oauth-code', async () => {
|
||||
logger.info('starting google sign in ...');
|
||||
shell.openExternal(oauthEndpoint);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleOpenUrl = async (_: any, url: string) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows().find(
|
||||
w => !w.isDestroyed()
|
||||
);
|
||||
const urlObj = parse(url.replace('??', '?'), true);
|
||||
if (!mainWindow || !url.startsWith('affine://auth-callback')) return;
|
||||
const code = urlObj.query['code'] as string;
|
||||
if (!code) return;
|
||||
|
||||
logger.info('google sign in code received from callback', code);
|
||||
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
resolve(getExchangeTokenParams(code));
|
||||
};
|
||||
|
||||
app.on('open-url', handleOpenUrl);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timed out'));
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
}, 30000);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('main:env-update', async (_, env, value) => {
|
||||
process.env[env] = value;
|
||||
});
|
||||
}
|
||||
|
||||
function registerDBHandlers() {
|
||||
app.on('activate', () => {
|
||||
for (const [_, workspaceDB] of dbMapping) {
|
||||
workspaceDB.reconnectDB();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('db:get-doc', async (_, id) => {
|
||||
logger.log('main: get doc', id);
|
||||
const workspaceDB = await ensureWorkspaceDB(id);
|
||||
return workspaceDB.getEncodedDocUpdates();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:apply-doc-update', async (_, id, update) => {
|
||||
logger.log('main: apply doc update', id);
|
||||
const workspaceDB = await ensureWorkspaceDB(id);
|
||||
return workspaceDB.applyUpdate(update);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:add-blob', async (_, workspaceId, key, data) => {
|
||||
logger.log('main: add blob', workspaceId, key);
|
||||
const workspaceDB = await ensureWorkspaceDB(workspaceId);
|
||||
return workspaceDB.addBlob(key, data);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:get-blob', async (_, workspaceId, key) => {
|
||||
logger.log('main: get blob', workspaceId, key);
|
||||
const workspaceDB = await ensureWorkspaceDB(workspaceId);
|
||||
return workspaceDB.getBlob(key);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:get-persisted-blobs', async (_, workspaceId) => {
|
||||
logger.log('main: get persisted blob keys', workspaceId);
|
||||
const workspaceDB = await ensureWorkspaceDB(workspaceId);
|
||||
return workspaceDB.getPersistentBlobKeys();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:delete-blob', async (_, workspaceId, key) => {
|
||||
logger.log('main: delete blob', workspaceId, key);
|
||||
const workspaceDB = await ensureWorkspaceDB(workspaceId);
|
||||
return workspaceDB.deleteBlob(key);
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:open-db-folder', async _ => {
|
||||
const workspaceDB = await ensureWorkspaceDB(currentWorkspaceId);
|
||||
logger.log('main: open db folder', workspaceDB.path);
|
||||
shell.showItemInFolder(workspaceDB.path);
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:open-load-db-file-dialog', async () => {
|
||||
// todo
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:open-save-db-file-dialog', async () => {
|
||||
logger.log('main: open save db file dialog', currentWorkspaceId);
|
||||
const workspaceDB = await ensureWorkspaceDB(currentWorkspaceId);
|
||||
const ret = await dialog.showSaveDialog({
|
||||
properties: ['showOverwriteConfirmation'],
|
||||
title: 'Save Workspace',
|
||||
buttonLabel: 'Save',
|
||||
defaultPath: currentWorkspaceId + '.db',
|
||||
message: 'Save Workspace as SQLite Database',
|
||||
});
|
||||
const filePath = ret.filePath;
|
||||
if (ret.canceled || !filePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await exportDatabase(workspaceDB, filePath);
|
||||
shell.showItemInFolder(filePath);
|
||||
return filePath;
|
||||
});
|
||||
}
|
||||
|
||||
export const registerHandlers = () => {
|
||||
registerWorkspaceHandlers();
|
||||
registerUIHandlers();
|
||||
registerDBHandlers();
|
||||
};
|
||||
@@ -3,7 +3,8 @@ import './security-restrictions';
|
||||
import { app } from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
import { registerHandlers } from './app-state';
|
||||
import { logger } from '../../logger';
|
||||
import { registerHandlers } from './handlers';
|
||||
import { restoreOrCreateWindow } from './main-window';
|
||||
import { registerProtocol } from './protocol';
|
||||
|
||||
@@ -22,6 +23,7 @@ if (process.defaultApp) {
|
||||
*/
|
||||
const isSingleInstance = app.requestSingleInstanceLock();
|
||||
if (!isSingleInstance) {
|
||||
logger.info('Another instance is running, exiting...');
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BrowserWindow, nativeTheme } from 'electron';
|
||||
import electronWindowState from 'electron-window-state';
|
||||
import { join } from 'path';
|
||||
|
||||
import { logger } from '../../logger';
|
||||
import { isMacOS } from '../../utils';
|
||||
|
||||
const IS_DEV = process.env.NODE_ENV === 'development';
|
||||
@@ -87,7 +88,7 @@ export async function restoreOrCreateWindow() {
|
||||
browserWindow.restore();
|
||||
}
|
||||
|
||||
browserWindow.focus();
|
||||
logger.info('Create main window');
|
||||
|
||||
return browserWindow;
|
||||
}
|
||||
|
||||
2
apps/electron/layers/preload/preload.d.ts
vendored
2
apps/electron/layers/preload/preload.d.ts
vendored
@@ -7,6 +7,6 @@ interface Window {
|
||||
*
|
||||
* @see https://github.com/cawa-93/dts-for-context-bridge
|
||||
*/
|
||||
readonly apis: { workspaceSync: (id: string) => Promise<any>; onThemeChange: (theme: string) => Promise<any>; onSidebarVisibilityChange: (visible: boolean) => Promise<any>; getGoogleOauthCode: () => Promise<{ requestInit: RequestInit; url: string; }>; updateEnv: (env: string, value: string) => void; };
|
||||
readonly apis: { db: { getDoc: (id: string) => Promise<Uint8Array>; applyDocUpdate: (id: string, update: Uint8Array) => Promise<any>; addBlob: (workspaceId: string, key: string, data: Uint8Array) => Promise<any>; getBlob: (workspaceId: string, key: string) => Promise<Uint8Array>; deleteBlob: (workspaceId: string, key: string) => Promise<any>; getPersistedBlobs: (workspaceId: string) => Promise<string[]>; }; workspace: { list: () => Promise<string[]>; delete: (id: string) => Promise<void>; }; openLoadDBFileDialog: () => Promise<any>; openSaveDBFileDialog: () => Promise<any>; onThemeChange: (theme: string) => Promise<any>; onSidebarVisibilityChange: (visible: boolean) => Promise<any>; onWorkspaceChange: (workspaceId: string) => Promise<any>; openDBFolder: () => Promise<any>; getGoogleOauthCode: () => Promise<{ requestInit: RequestInit; url: string; }>; updateEnv: (env: string, value: string) => void; };
|
||||
readonly appInfo: { electron: boolean; isMacOS: boolean; };
|
||||
}
|
||||
|
||||
@@ -22,7 +22,32 @@ import { isMacOS } from '../../utils';
|
||||
* @see https://github.com/cawa-93/dts-for-context-bridge
|
||||
*/
|
||||
contextBridge.exposeInMainWorld('apis', {
|
||||
workspaceSync: (id: string) => ipcRenderer.invoke('octo:workspace-sync', id),
|
||||
db: {
|
||||
// TODO: do we need to store the workspace list locally?
|
||||
// workspace providers
|
||||
getDoc: (id: string): Promise<Uint8Array | null> =>
|
||||
ipcRenderer.invoke('db:get-doc', id),
|
||||
applyDocUpdate: (id: string, update: Uint8Array) =>
|
||||
ipcRenderer.invoke('db:apply-doc-update', id, update),
|
||||
addBlob: (workspaceId: string, key: string, data: Uint8Array) =>
|
||||
ipcRenderer.invoke('db:add-blob', workspaceId, key, data),
|
||||
getBlob: (workspaceId: string, key: string): Promise<Uint8Array | null> =>
|
||||
ipcRenderer.invoke('db:get-blob', workspaceId, key),
|
||||
deleteBlob: (workspaceId: string, key: string) =>
|
||||
ipcRenderer.invoke('db:delete-blob', workspaceId, key),
|
||||
getPersistedBlobs: (workspaceId: string): Promise<string[]> =>
|
||||
ipcRenderer.invoke('db:get-persisted-blobs', workspaceId),
|
||||
},
|
||||
|
||||
workspace: {
|
||||
list: (): Promise<string[]> => ipcRenderer.invoke('workspace:list'),
|
||||
delete: (id: string): Promise<void> =>
|
||||
ipcRenderer.invoke('workspace:delete', id),
|
||||
// create will be implicitly called by db functions
|
||||
},
|
||||
|
||||
openLoadDBFileDialog: () => ipcRenderer.invoke('ui:open-load-db-file-dialog'),
|
||||
openSaveDBFileDialog: () => ipcRenderer.invoke('ui:open-save-db-file-dialog'),
|
||||
|
||||
// ui
|
||||
onThemeChange: (theme: string) =>
|
||||
@@ -31,6 +56,11 @@ contextBridge.exposeInMainWorld('apis', {
|
||||
onSidebarVisibilityChange: (visible: boolean) =>
|
||||
ipcRenderer.invoke('ui:sidebar-visibility-change', visible),
|
||||
|
||||
onWorkspaceChange: (workspaceId: string) =>
|
||||
ipcRenderer.invoke('ui:workspace-change', workspaceId),
|
||||
|
||||
openDBFolder: () => ipcRenderer.invoke('ui:open-db-folder'),
|
||||
|
||||
/**
|
||||
* Try sign in using Google and return a request object to exchange the code for a token
|
||||
* Not exchange in Node side because it is easier to do it in the renderer with VPN
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
"@electron-forge/shared-types": "^6.1.1",
|
||||
"@electron/rebuild": "^3.2.12",
|
||||
"@electron/remote": "2.0.9",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"dts-for-context-bridge": "^0.7.1",
|
||||
"electron": "24.1.2",
|
||||
"electron-squirrel-startup": "1.0.0",
|
||||
@@ -38,11 +40,12 @@
|
||||
"zx": "^7.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"electron-log": "^5.0.0-beta.22",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"firebase": "^9.19.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"undici": "^5.21.2"
|
||||
"sqlite3": "^5.1.6",
|
||||
"undici": "^5.21.2",
|
||||
"yjs": "^13.5.53"
|
||||
},
|
||||
"build": {
|
||||
"protocols": [
|
||||
@@ -54,6 +57,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@3.5.0",
|
||||
"stableVersion": "0.5.3"
|
||||
"stableVersion": "0.5.3",
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/electron/resources/icons/dmg-background.png
Normal file
BIN
apps/electron/resources/icons/dmg-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
apps/electron/resources/icons/dmg-background@2x.png
Normal file
BIN
apps/electron/resources/icons/dmg-background@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 342 KiB |
@@ -1,4 +1,3 @@
|
||||
|
||||
const NODE_MAJOR_VERSION = 18;
|
||||
|
||||
const nativeNodeModulesPlugin = {
|
||||
@@ -29,7 +28,7 @@ export default () => {
|
||||
bundle: true,
|
||||
target: `node${NODE_MAJOR_VERSION}`,
|
||||
platform: 'node',
|
||||
external: ['electron'],
|
||||
external: ['electron', 'sqlite3'],
|
||||
plugins: [nativeNodeModulesPlugin],
|
||||
define: define,
|
||||
},
|
||||
|
||||
@@ -33,39 +33,44 @@ if (process.platform === 'win32') {
|
||||
$.shell = 'powershell.exe';
|
||||
$.prefix = '';
|
||||
}
|
||||
// step 1: build web (nextjs) dist
|
||||
process.env.ENABLE_LEGACY_PROVIDER = 'false';
|
||||
|
||||
cd(repoRootDir);
|
||||
await $`yarn add`;
|
||||
await $`yarn build`;
|
||||
await $`yarn export`;
|
||||
|
||||
// step 1.5: amend sourceMappingURL to allow debugging in devtools
|
||||
await glob('**/*.{js,css}', { cwd: affineWebOutDir }).then(files => {
|
||||
return files.map(async file => {
|
||||
const dir = path.dirname(file);
|
||||
const fullpath = path.join(affineWebOutDir, file);
|
||||
let content = await fs.readFile(fullpath, 'utf-8');
|
||||
// replace # sourceMappingURL=76-6370cd185962bc89.js.map
|
||||
// to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map
|
||||
content = content.replace(/# sourceMappingURL=(.*)\.map/g, (_, p1) => {
|
||||
return `# sourceMappingURL=assets://./${dir}/${p1}.map`;
|
||||
});
|
||||
await fs.writeFile(fullpath, content);
|
||||
});
|
||||
});
|
||||
|
||||
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
|
||||
|
||||
// step 2: build electron resources
|
||||
// step 1: build electron resources
|
||||
await buildLayers();
|
||||
echo('Build layers done');
|
||||
|
||||
// step 2: build web (nextjs) dist
|
||||
if (!process.env.SKIP_WEB_BUILD) {
|
||||
process.env.ENABLE_LEGACY_PROVIDER = 'false';
|
||||
await $`yarn build`;
|
||||
await $`yarn export`;
|
||||
|
||||
// step 1.5: amend sourceMappingURL to allow debugging in devtools
|
||||
await glob('**/*.{js,css}', { cwd: affineWebOutDir }).then(files => {
|
||||
return files.map(async file => {
|
||||
const dir = path.dirname(file);
|
||||
const fullpath = path.join(affineWebOutDir, file);
|
||||
let content = await fs.readFile(fullpath, 'utf-8');
|
||||
// replace # sourceMappingURL=76-6370cd185962bc89.js.map
|
||||
// to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map
|
||||
content = content.replace(/# sourceMappingURL=(.*)\.map/g, (_, p1) => {
|
||||
return `# sourceMappingURL=assets://./${dir}/${p1}.map`;
|
||||
});
|
||||
await fs.writeFile(fullpath, content);
|
||||
});
|
||||
});
|
||||
|
||||
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
|
||||
}
|
||||
|
||||
/// --------
|
||||
/// --------
|
||||
/// --------
|
||||
async function cleanup() {
|
||||
await fs.emptyDir(publicAffineOutDir);
|
||||
if (!process.env.SKIP_WEB_BUILD) {
|
||||
await fs.emptyDir(publicAffineOutDir);
|
||||
}
|
||||
await fs.emptyDir(path.join(electronRootDir, 'layers', 'main', 'dist'));
|
||||
await fs.emptyDir(path.join(electronRootDir, 'layers', 'preload', 'dist'));
|
||||
await fs.remove(path.join(electronRootDir, 'out'));
|
||||
|
||||
Reference in New Issue
Block a user