refactor: new project struct (#8199)

packages/frontend/web -> packages/frontend/apps/web
packages/frontend/mobile -> packages/frontend/apps/mobile
packages/frontend/electron -> packages/frontend/apps/electron
This commit is contained in:
EYHN
2024-09-12 07:42:57 +00:00
parent 7c4eab6cd3
commit cc5a6e6d40
291 changed files with 139 additions and 134 deletions
@@ -0,0 +1,4 @@
export interface AuthenticationRequest {
method: 'magic-link' | 'oauth';
payload: Record<string, any>;
}
@@ -0,0 +1,114 @@
import { Menu } from 'electron';
import { logger } from '../logger';
import {
addTab,
closeTab,
reloadView,
type TabAction,
WebContentViewsManager,
} from './tab-views';
export const showTabContextMenu = async (tabId: string, viewIndex: number) => {
const workbenches = WebContentViewsManager.instance.tabViewsMeta.workbenches;
const tabMeta = workbenches.find(w => w.id === tabId);
if (!tabMeta) {
return;
}
const { resolve, promise } = Promise.withResolvers<TabAction | null>();
const template: Parameters<typeof Menu.buildFromTemplate>[0] = [
tabMeta.pinned
? {
label: 'Unpin tab',
click: () => {
WebContentViewsManager.instance.pinTab(tabId, false);
},
}
: {
label: 'Pin tab',
click: () => {
WebContentViewsManager.instance.pinTab(tabId, true);
},
},
{
label: 'Refresh tab',
click: () => {
reloadView().catch(logger.error);
},
},
{
label: 'Duplicate tab',
click: () => {
addTab({
basename: tabMeta.basename,
view: tabMeta.views,
show: false,
}).catch(logger.error);
},
},
{ type: 'separator' },
tabMeta.views.length > 1
? {
label: 'Separate tabs',
click: () => {
WebContentViewsManager.instance.separateView(tabId, viewIndex);
},
}
: {
label: 'Open in split view',
click: () => {
WebContentViewsManager.instance.openInSplitView({ tabId });
},
},
...(workbenches.length > 1
? ([
{ type: 'separator' },
{
label: 'Close tab',
click: () => {
closeTab(tabId).catch(logger.error);
},
},
{
label: 'Close other tabs',
click: () => {
const tabsToRetain =
WebContentViewsManager.instance.tabViewsMeta.workbenches.filter(
w => w.id === tabId || w.pinned
);
WebContentViewsManager.instance.patchTabViewsMeta({
workbenches: tabsToRetain,
activeWorkbenchId: tabId,
});
},
},
] as const)
: []),
];
const menu = Menu.buildFromTemplate(template);
menu.popup();
// eslint-disable-next-line prefer-const
let unsub: (() => void) | undefined;
const subscription = WebContentViewsManager.instance.tabAction$.subscribe(
action => {
resolve(action);
unsub?.();
}
);
menu.on('menu-will-close', () => {
setTimeout(() => {
resolve(null);
unsub?.();
});
});
unsub = () => {
subscription.unsubscribe();
};
return promise;
};
@@ -0,0 +1,71 @@
import { join } from 'node:path';
import { BrowserWindow, type Display, screen } from 'electron';
import { isMacOS } from '../../shared/utils';
import { customThemeViewUrl } from '../constants';
import { logger } from '../logger';
let customThemeWindow: Promise<BrowserWindow> | undefined;
const getScreenSize = (display: Display) => {
const { width, height } = isMacOS() ? display.bounds : display.workArea;
return { width, height };
};
async function createCustomThemeWindow(additionalArguments: string[]) {
logger.info('creating custom theme window');
const { width: maxWidth, height: maxHeight } = getScreenSize(
screen.getPrimaryDisplay()
);
const browserWindow = new BrowserWindow({
width: Math.min(maxWidth, 800),
height: Math.min(maxHeight, 600),
resizable: true,
maximizable: false,
fullscreenable: false,
webPreferences: {
webgl: true,
preload: join(__dirname, './preload.js'),
additionalArguments: additionalArguments,
},
});
await browserWindow.loadURL(customThemeViewUrl);
browserWindow.on('closed', () => {
customThemeWindow = undefined;
});
return browserWindow;
}
const getWindowAdditionalArguments = async () => {
const { getExposedMeta } = await import('../exposed');
const mainExposedMeta = getExposedMeta();
return [
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
`--window-name=theme-editor`,
];
};
export async function getOrCreateCustomThemeWindow() {
const additionalArguments = await getWindowAdditionalArguments();
if (
!customThemeWindow ||
(await customThemeWindow.then(w => w.isDestroyed()))
) {
customThemeWindow = createCustomThemeWindow(additionalArguments);
}
return customThemeWindow;
}
export async function getCustomThemeWindow() {
if (!customThemeWindow) return;
const window = await customThemeWindow;
if (window.isDestroyed()) return;
return window;
}
@@ -0,0 +1,7 @@
export * from './authentication';
export * from './launcher';
export * from './main-window';
export * from './onboarding';
export * from './stage';
export * from './tab-views';
export * from './types';
@@ -0,0 +1,24 @@
import { logger } from '../logger';
import { initAndShowMainWindow } from './main-window';
import { getOnboardingWindow, getOrCreateOnboardingWindow } from './onboarding';
import { launchStage } from './stage';
/**
* Launch app depending on launch stage
*/
export async function launch() {
const stage = launchStage.value;
if (stage === 'main') {
initAndShowMainWindow().catch(e => {
logger.error('Failed to restore or create window:', e);
});
getOnboardingWindow()
.then(w => w?.destroy())
.catch(e => logger.error('Failed to destroy onboarding window:', e));
}
if (stage === 'onboarding')
getOrCreateOnboardingWindow().catch(e => {
logger.error('Failed to restore or create onboarding window:', e);
});
}
@@ -0,0 +1,289 @@
import { join } from 'node:path';
import { BrowserWindow, nativeTheme } from 'electron';
import electronWindowState from 'electron-window-state';
import { BehaviorSubject } from 'rxjs';
import { isLinux, isMacOS, isWindows } from '../../shared/utils';
import { buildType } from '../config';
import { mainWindowOrigin } from '../constants';
import { ensureHelperProcess } from '../helper-process';
import { logger } from '../logger';
import { uiSubjects } from '../ui/subject';
const IS_DEV: boolean =
process.env.NODE_ENV === 'development' && !process.env.CI;
function closeAllWindows() {
BrowserWindow.getAllWindows().forEach(w => {
if (!w.isDestroyed()) {
w.destroy();
}
});
}
export class MainWindowManager {
static readonly instance = new MainWindowManager();
mainWindowReady: Promise<BrowserWindow> | undefined;
mainWindow$ = new BehaviorSubject<BrowserWindow | undefined>(undefined);
private hiddenMacWindow: BrowserWindow | undefined;
get mainWindow() {
return this.mainWindow$.value;
}
// #region private methods
private preventMacAppQuit() {
if (!this.hiddenMacWindow && isMacOS()) {
this.hiddenMacWindow = new BrowserWindow({
show: false,
width: 100,
height: 100,
});
this.hiddenMacWindow.on('close', () => {
this.cleanupWindows();
});
}
}
private cleanupWindows() {
closeAllWindows();
this.mainWindowReady = undefined;
this.mainWindow$.next(undefined);
this.hiddenMacWindow?.destroy();
this.hiddenMacWindow = undefined;
}
private async createMainWindow() {
logger.info('create window');
const mainWindowState = electronWindowState({
defaultWidth: 1000,
defaultHeight: 800,
});
await ensureHelperProcess();
const browserWindow = new BrowserWindow({
titleBarStyle: isMacOS()
? 'hiddenInset'
: isWindows()
? 'hidden'
: 'default',
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
autoHideMenuBar: isLinux(),
minWidth: 640,
minHeight: 480,
visualEffectState: 'active',
vibrancy: 'under-window',
// backgroundMaterial: 'mica',
height: mainWindowState.height,
show: false, // Use 'ready-to-show' event to show window
webPreferences: {
webgl: true,
contextIsolation: true,
sandbox: false,
},
});
if (isLinux()) {
browserWindow.setIcon(
// __dirname is `packages/frontend/apps/electron/dist` (the bundled output directory)
join(__dirname, `../resources/icons/icon_${buildType}_64x64.png`)
);
}
nativeTheme.themeSource = 'light';
mainWindowState.manage(browserWindow);
this.bindEvents(browserWindow);
return browserWindow;
}
private bindEvents(mainWindow: BrowserWindow) {
/**
* If you install `show: true` then it can cause issues when trying to close the window.
* Use `show: false` and listener events `ready-to-show` to fix these issues.
*
* @see https://github.com/electron/electron/issues/25012
*/
mainWindow.on('ready-to-show', () => {
logger.info('main window is ready to show');
uiSubjects.onMaximized$.next(mainWindow.isMaximized());
uiSubjects.onFullScreen$.next(mainWindow.isFullScreen());
});
mainWindow.on('close', e => {
// TODO(@pengx17): gracefully close the app, for example, ask user to save unsaved changes
e.preventDefault();
if (!isMacOS()) {
closeAllWindows();
} else {
// hide window on macOS
// application quit will be handled by closing the hidden window
//
// explanation:
// - closing the top window (by clicking close button or CMD-w)
// - will be captured in "close" event here
// - hiding the app to make the app open faster when user click the app icon
// - quit the app by "cmd+q" or right click on the dock icon and select "quit"
// - all browser windows will capture the "close" event
// - the hidden window will close all windows
// - "window-all-closed" event will be emitted and eventually quit the app
if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => {
mainWindow.hide();
});
mainWindow.setFullScreen(false);
} else {
mainWindow.hide();
}
}
});
const refreshBound = (timeout = 0) => {
setTimeout(() => {
// FIXME: workaround for theme bug in full screen mode
const size = mainWindow.getSize();
mainWindow.setSize(size[0] + 1, size[1] + 1);
mainWindow.setSize(size[0], size[1]);
}, timeout);
};
mainWindow.on('leave-full-screen', () => {
// seems call this too soon may cause the app to crash
refreshBound();
refreshBound(1000);
uiSubjects.onMaximized$.next(false);
uiSubjects.onFullScreen$.next(false);
});
mainWindow.on('maximize', () => {
uiSubjects.onMaximized$.next(true);
});
mainWindow.on('unmaximize', () => {
uiSubjects.onMaximized$.next(false);
});
// full-screen == maximized in UI on windows
mainWindow.on('enter-full-screen', () => {
uiSubjects.onFullScreen$.next(true);
});
mainWindow.on('leave-full-screen', () => {
uiSubjects.onFullScreen$.next(false);
});
}
// #endregion
async ensureMainWindow(): Promise<BrowserWindow> {
if (
!this.mainWindowReady ||
(await this.mainWindowReady.then(w => w.isDestroyed()))
) {
this.mainWindowReady = this.createMainWindow();
this.mainWindow$.next(await this.mainWindowReady);
this.preventMacAppQuit();
}
return this.mainWindowReady;
}
/**
* Init main BrowserWindow. Will create a new window if it's not created yet.
*/
async initAndShowMainWindow() {
const mainWindow = await this.ensureMainWindow();
if (IS_DEV) {
// do not gain focus in dev mode
mainWindow.showInactive();
} else {
mainWindow.show();
}
this.preventMacAppQuit();
return mainWindow;
}
}
export async function initAndShowMainWindow() {
return MainWindowManager.instance.initAndShowMainWindow();
}
export async function getMainWindow() {
return MainWindowManager.instance.ensureMainWindow();
}
export async function showMainWindow() {
const window = await getMainWindow();
if (!window) return;
if (window.isMinimized()) {
window.restore();
}
window.focus();
}
const getWindowAdditionalArguments = async () => {
const { getExposedMeta } = await import('../exposed');
const mainExposedMeta = getExposedMeta();
return [
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
`--window-name=hidden-window`,
];
};
function transformToAppUrl(url: URL) {
const params = url.searchParams;
return mainWindowOrigin + url.pathname + '?' + params.toString();
}
/**
* Open a URL in a hidden window.
*/
export async function openUrlInHiddenWindow(urlObj: URL) {
const url = transformToAppUrl(urlObj);
const win = new BrowserWindow({
width: 1200,
height: 600,
webPreferences: {
preload: join(__dirname, './preload.js'),
additionalArguments: await getWindowAdditionalArguments(),
},
show: environment.isDebug,
});
if (environment.isDebug) {
win.webContents.openDevTools({
mode: 'detach',
});
}
win.on('close', e => {
e.preventDefault();
if (win && !win.isDestroyed()) {
win.destroy();
}
});
logger.info('loading page at', url);
win.loadURL(url).catch(e => {
logger.error('failed to load url', e);
});
return win;
}
// TODO(@pengx17): somehow the page won't load the url passed, help needed
export async function openUrlInMainWindow(urlObj: URL) {
const url = transformToAppUrl(urlObj);
logger.info('loading page at', url);
const mainWindow = await getMainWindow();
if (mainWindow) {
await mainWindow.loadURL(url);
}
return null;
}
@@ -0,0 +1,117 @@
import { join } from 'node:path';
import type { Display } from 'electron';
import { BrowserWindow, screen } from 'electron';
import { isMacOS } from '../../shared/utils';
import { onboardingViewUrl } from '../constants';
// import { getExposedMeta } from './exposed';
import { logger } from '../logger';
const getScreenSize = (display: Display) => {
const { width, height } = isMacOS() ? display.bounds : display.workArea;
return { width, height };
};
// todo: not all window need all of the exposed meta
const getWindowAdditionalArguments = async () => {
const { getExposedMeta } = await import('../exposed');
const mainExposedMeta = getExposedMeta();
return [
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
`--window-name=onboarding`,
];
};
function fullscreenAndCenter(browserWindow: BrowserWindow) {
const position = browserWindow.getPosition();
const size = browserWindow.getSize();
const currentDisplay = screen.getDisplayNearestPoint({
x: position[0] + size[0] / 2,
y: position[1] + size[1] / 2,
});
if (!currentDisplay) return;
const { width, height } = getScreenSize(currentDisplay);
browserWindow.setSize(width, height);
browserWindow.center();
}
async function createOnboardingWindow(additionalArguments: string[]) {
logger.info('creating onboarding window');
// get user's screen size
const { width, height } = getScreenSize(screen.getPrimaryDisplay());
const browserWindow = new BrowserWindow({
width,
height,
frame: false,
show: false,
resizable: false,
closable: false,
minimizable: false,
movable: false,
titleBarStyle: 'hidden',
maximizable: false,
fullscreenable: false,
// skipTaskbar: true,
transparent: true,
hasShadow: false,
roundedCorners: false,
webPreferences: {
webgl: true,
preload: join(__dirname, './preload.js'),
additionalArguments: additionalArguments,
},
});
// workaround for the phantom title bar on windows when losing focus
// see https://github.com/electron/electron/issues/39959#issuecomment-1758736966
browserWindow.on('focus', () => {
browserWindow.setBackgroundColor('#00000000');
});
browserWindow.on('blur', () => {
browserWindow.setBackgroundColor('#00000000');
});
browserWindow.on('ready-to-show', () => {
// forcing zoom factor to 1 to avoid onboarding display issues
browserWindow.webContents.setZoomFactor(1);
fullscreenAndCenter(browserWindow);
// TODO(@catsjuice): add a timeout to avoid flickering, window is ready, but dom is not ready
setTimeout(() => {
browserWindow.show();
}, 300);
});
// When moved to another screen, resize to fit the screen
browserWindow.on('moved', () => {
fullscreenAndCenter(browserWindow);
});
await browserWindow.loadURL(onboardingViewUrl);
return browserWindow;
}
let onBoardingWindow: Promise<BrowserWindow> | undefined;
export async function getOrCreateOnboardingWindow() {
const additionalArguments = await getWindowAdditionalArguments();
if (
!onBoardingWindow ||
(await onBoardingWindow.then(w => w.isDestroyed()))
) {
onBoardingWindow = createOnboardingWindow(additionalArguments);
}
return onBoardingWindow;
}
export async function getOnboardingWindow() {
if (!onBoardingWindow) return;
const window = await onBoardingWindow;
if (window.isDestroyed()) return;
return window;
}
@@ -0,0 +1,6 @@
import { persistentConfig } from '../config-storage/persist';
import type { LaunchStage } from './types';
export const launchStage: { value: LaunchStage } = {
value: persistentConfig.get('onBoarding') ? 'onboarding' : 'main',
};
@@ -0,0 +1,45 @@
import { z } from 'zod';
export const workbenchViewIconNameSchema = z.enum([
'trash',
'allDocs',
'collection',
'tag',
'doc', // refers to a doc whose mode is not yet being resolved
'page',
'edgeless',
'journal',
]);
export const workbenchViewMetaSchema = z.object({
id: z.string(),
path: z
.object({
pathname: z.string().optional(),
hash: z.string().optional(),
search: z.string().optional(),
})
.optional(),
// todo: move title/module to cached stated
title: z.string().optional(),
iconName: workbenchViewIconNameSchema.optional(),
});
export const workbenchMetaSchema = z.object({
id: z.string(),
activeViewIndex: z.number(),
pinned: z.boolean().optional(),
basename: z.string(),
views: z.array(workbenchViewMetaSchema),
});
export const tabViewsMetaSchema = z.object({
activeWorkbenchId: z.string().optional(),
workbenches: z.array(workbenchMetaSchema).default([]),
});
export const TabViewsMetaKey = 'tabViewsMetaSchema';
export type TabViewsMetaSchema = z.infer<typeof tabViewsMetaSchema>;
export type WorkbenchMeta = z.infer<typeof workbenchMetaSchema>;
export type WorkbenchViewMeta = z.infer<typeof workbenchViewMetaSchema>;
export type WorkbenchViewModule = z.infer<typeof workbenchViewIconNameSchema>;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
export type LaunchStage = 'main' | 'onboarding';