mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
feat(electron): onboarding at first launch logic for client and web (#5183)
- Added a simple abstraction of persistent storage class.
- Different persistence solutions are provided for web and client.
- web: stored in localStorage
- client: stored in the application directory as `.json` file
- Define persistent app-config schema
- Add a new hook that can interactive with persistent-app-config reactively
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import { persistentConfig } from './persist';
|
||||
|
||||
export const configStorageHandlers = {
|
||||
get: async () => persistentConfig.get(),
|
||||
set: async (_, v) => persistentConfig.set(v),
|
||||
} satisfies NamespaceHandlers;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './handlers';
|
||||
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
AppConfigStorage,
|
||||
defaultAppConfig,
|
||||
} from '@toeverything/infra/app-config-storage';
|
||||
import { app } from 'electron';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const FILENAME = 'config.json';
|
||||
const FILEPATH = path.join(app.getPath('userData'), FILENAME);
|
||||
|
||||
export const persistentConfig = new AppConfigStorage({
|
||||
config: defaultAppConfig,
|
||||
get: () => JSON.parse(fs.readFileSync(FILEPATH, 'utf-8')),
|
||||
set: data => fs.writeFileSync(FILEPATH, JSON.stringify(data, null, 2)),
|
||||
});
|
||||
1
packages/frontend/electron/src/main/constants.ts
Normal file
1
packages/frontend/electron/src/main/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const mainWindowOrigin = process.env.DEV_SERVER_URL || 'file://.';
|
||||
@@ -3,11 +3,11 @@ import path from 'node:path';
|
||||
import { type App, type BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import { buildType, CLOUD_BASE_URL, isDev } from './config';
|
||||
import { mainWindowOrigin } from './constants';
|
||||
import { logger } from './logger';
|
||||
import {
|
||||
getMainWindow,
|
||||
handleOpenUrlInHiddenWindow,
|
||||
mainWindowOrigin,
|
||||
removeCookie,
|
||||
setCookie,
|
||||
} from './main-window';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { app, BrowserWindow } from 'electron';
|
||||
|
||||
import { applicationMenuEvents } from './application-menu';
|
||||
import { logger } from './logger';
|
||||
import { uiEvents } from './ui';
|
||||
import { uiEvents } from './ui/events';
|
||||
import { updaterEvents } from './updater/event';
|
||||
|
||||
export const allEvents = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
ClipboardHandlerManager,
|
||||
ConfigStorageHandlerManager,
|
||||
DebugHandlerManager,
|
||||
ExportHandlerManager,
|
||||
UIHandlerManager,
|
||||
@@ -9,9 +10,10 @@ import type {
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import { clipboardHandlers } from './clipboard';
|
||||
import { configStorageHandlers } from './config-storage';
|
||||
import { exportHandlers } from './export';
|
||||
import { getLogFilePath, logger, revealLogFile } from './logger';
|
||||
import { uiHandlers } from './ui';
|
||||
import { uiHandlers } from './ui/handlers';
|
||||
import { updaterHandlers } from './updater';
|
||||
|
||||
export const debugHandlers = {
|
||||
@@ -44,6 +46,10 @@ type AllHandlers = {
|
||||
Electron.IpcMainInvokeEvent,
|
||||
UpdaterHandlerManager
|
||||
>;
|
||||
configStorage: UnwrapManagerHandlerToServerSide<
|
||||
Electron.IpcMainInvokeEvent,
|
||||
ConfigStorageHandlerManager
|
||||
>;
|
||||
};
|
||||
|
||||
// Note: all of these handlers will be the single-source-of-truth for the apis exposed to the renderer process
|
||||
@@ -53,6 +59,7 @@ export const allHandlers = {
|
||||
clipboard: clipboardHandlers,
|
||||
export: exportHandlers,
|
||||
updater: updaterHandlers,
|
||||
configStorage: configStorageHandlers,
|
||||
} satisfies AllHandlers;
|
||||
|
||||
export const registerHandlers = () => {
|
||||
|
||||
@@ -11,9 +11,10 @@ import { registerEvents } from './events';
|
||||
import { registerHandlers } from './handlers';
|
||||
import { ensureHelperProcess } from './helper-process';
|
||||
import { logger } from './logger';
|
||||
import { initMainWindow as initMainWindow } from './main-window';
|
||||
import { registerProtocol } from './protocol';
|
||||
import { registerUpdater } from './updater';
|
||||
import { launch } from './windows-manager/launcher';
|
||||
import { launchStage } from './windows-manager/stage';
|
||||
|
||||
app.enableSandbox();
|
||||
|
||||
@@ -26,11 +27,9 @@ if (overrideSession) {
|
||||
}
|
||||
|
||||
if (require('electron-squirrel-startup')) app.quit();
|
||||
// allow tests to overwrite app name through passing args
|
||||
if (process.argv.includes('--app-name')) {
|
||||
const appNameIndex = process.argv.indexOf('--app-name');
|
||||
const appName = process.argv[appNameIndex + 1];
|
||||
app.setName(appName);
|
||||
|
||||
if (process.env.SKIP_ONBOARDING) {
|
||||
launchStage.value = 'main';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,13 +52,9 @@ app.on('window-all-closed', () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://www.electronjs.org/docs/v14-x-y/api/app#event-activate-macos Event: 'activate'
|
||||
* @see https://www.electronjs.org/docs/latest/api/app#event-activate-macos Event: 'activate'
|
||||
*/
|
||||
app.on('activate', () => {
|
||||
initMainWindow().catch(e =>
|
||||
console.error('Failed to restore or create window:', e)
|
||||
);
|
||||
});
|
||||
app.on('activate', launch);
|
||||
|
||||
setupDeepLink(app);
|
||||
|
||||
@@ -72,7 +67,7 @@ app
|
||||
.then(registerHandlers)
|
||||
.then(registerEvents)
|
||||
.then(ensureHelperProcess)
|
||||
.then(initMainWindow)
|
||||
.then(launch)
|
||||
.then(createApplicationMenu)
|
||||
.then(registerUpdater)
|
||||
.catch(e => console.error('Failed create window:', e));
|
||||
|
||||
@@ -5,6 +5,7 @@ import electronWindowState from 'electron-window-state';
|
||||
import { join } from 'path';
|
||||
|
||||
import { isMacOS, isWindows } from '../shared/utils';
|
||||
import { mainWindowOrigin } from './constants';
|
||||
import { ensureHelperProcess } from './helper-process';
|
||||
import { logger } from './logger';
|
||||
import { uiSubjects } from './ui/subject';
|
||||
@@ -15,8 +16,6 @@ const IS_DEV: boolean =
|
||||
|
||||
const DEV_TOOL = process.env.DEV_TOOL === 'true';
|
||||
|
||||
export const mainWindowOrigin = process.env.DEV_SERVER_URL || 'file://.';
|
||||
|
||||
// todo: not all window need all of the exposed meta
|
||||
const getWindowAdditionalArguments = async () => {
|
||||
const { getExposedMeta } = await import('./exposed');
|
||||
|
||||
78
packages/frontend/electron/src/main/onboarding.ts
Normal file
78
packages/frontend/electron/src/main/onboarding.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { assert } from 'console';
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { join } from 'path';
|
||||
|
||||
import { mainWindowOrigin } from './constants';
|
||||
// import { getExposedMeta } from './exposed';
|
||||
import { ensureHelperProcess } from './helper-process';
|
||||
import { logger } from './logger';
|
||||
|
||||
// todo: not all window need all of the exposed meta
|
||||
const getWindowAdditionalArguments = async () => {
|
||||
const { getExposedMeta } = await import('./exposed');
|
||||
const mainExposedMeta = getExposedMeta();
|
||||
const helperProcessManager = await ensureHelperProcess();
|
||||
const helperExposedMeta = await helperProcessManager.rpc?.getMeta();
|
||||
return [
|
||||
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
|
||||
`--helper-exposed-meta=` + JSON.stringify(helperExposedMeta),
|
||||
];
|
||||
};
|
||||
|
||||
async function createOnboardingWindow(additionalArguments: string[]) {
|
||||
logger.info('creating onboarding window');
|
||||
|
||||
const helperProcessManager = await ensureHelperProcess();
|
||||
const helperExposedMeta = await helperProcessManager.rpc?.getMeta();
|
||||
|
||||
assert(helperExposedMeta, 'helperExposedMeta should be defined');
|
||||
|
||||
const browserWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
frame: false,
|
||||
show: false,
|
||||
closable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
fullscreenable: false,
|
||||
skipTaskbar: true,
|
||||
// transparent: true,
|
||||
webPreferences: {
|
||||
webgl: true,
|
||||
preload: join(__dirname, './preload.js'),
|
||||
additionalArguments: additionalArguments,
|
||||
},
|
||||
});
|
||||
|
||||
browserWindow.on('ready-to-show', () => {
|
||||
browserWindow.show();
|
||||
});
|
||||
|
||||
await browserWindow.loadURL(
|
||||
`${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}onboarding`
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -2,9 +2,12 @@ import { app, nativeTheme } from 'electron';
|
||||
import { getLinkPreview } from 'link-preview-js';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { persistentConfig } from '../config-storage/persist';
|
||||
import { logger } from '../logger';
|
||||
import { getMainWindow } from '../main-window';
|
||||
import { getMainWindow, initMainWindow } from '../main-window';
|
||||
import { getOnboardingWindow } from '../onboarding';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import { launchStage } from '../windows-manager/stage';
|
||||
import { getChallengeResponse } from './challenge';
|
||||
import { getGoogleOauthCode } from './google-auth';
|
||||
|
||||
@@ -46,6 +49,16 @@ export const uiHandlers = {
|
||||
getChallengeResponse: async (_, challenge: string) => {
|
||||
return getChallengeResponse(challenge);
|
||||
},
|
||||
handleOpenMainApp: async () => {
|
||||
if (launchStage.value === 'onboarding') {
|
||||
launchStage.value = 'main';
|
||||
persistentConfig.patch('onBoarding', false);
|
||||
}
|
||||
initMainWindow().catch(logger.error);
|
||||
getOnboardingWindow()
|
||||
.then(w => w?.destroy())
|
||||
.catch(logger.error);
|
||||
},
|
||||
getBookmarkDataByLink: async (_, link: string) => {
|
||||
if (
|
||||
(link.startsWith('https://x.com/') ||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { initMainWindow } from '../main-window';
|
||||
import {
|
||||
getOnboardingWindow,
|
||||
getOrCreateOnboardingWindow,
|
||||
} from '../onboarding';
|
||||
import { launchStage } from './stage';
|
||||
|
||||
/**
|
||||
* Launch app depending on launch stage
|
||||
*/
|
||||
export function launch() {
|
||||
const stage = launchStage.value;
|
||||
if (stage === 'main') {
|
||||
initMainWindow().catch(e => {
|
||||
console.error('Failed to restore or create window:', e);
|
||||
});
|
||||
|
||||
getOnboardingWindow()
|
||||
.then(w => w?.destroy())
|
||||
.catch(e => console.error('Failed to destroy onboarding window:', e));
|
||||
}
|
||||
if (stage === 'onboarding')
|
||||
getOrCreateOnboardingWindow().catch(e => {
|
||||
console.error('Failed to restore or create onboarding window:', e);
|
||||
});
|
||||
}
|
||||
@@ -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 @@
|
||||
export type LaunchStage = 'main' | 'onboarding';
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { BrowserWindow } from 'electron';
|
||||
|
||||
import type { LaunchStage } from './types';
|
||||
|
||||
export const windows$: Record<LaunchStage, Promise<BrowserWindow> | undefined> =
|
||||
{
|
||||
main: undefined,
|
||||
onboarding: undefined,
|
||||
};
|
||||
Reference in New Issue
Block a user