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:
Cats Juice
2023-12-19 07:17:54 +00:00
parent e0d328676d
commit 15dd20ef48
32 changed files with 470 additions and 29 deletions

View File

@@ -0,0 +1,88 @@
import { z } from 'zod';
const _appConfigSchema = z.object({
/** whether to show onboarding first */
onBoarding: z.boolean().optional().default(true),
});
export type AppConfigSchema = z.infer<typeof _appConfigSchema>;
export const defaultAppConfig = _appConfigSchema.parse({});
const _storage: Record<number, any> = {};
let _inMemoryId = 0;
interface StorageOptions<T> {
/** default config */
config: T;
get?: () => T;
set?: (data: T) => void;
}
/**
* Storage for app configuration, stored in memory by default
*/
class Storage<T> {
private _cfg: T;
private readonly _id = _inMemoryId++;
private readonly _options;
constructor(options: StorageOptions<T>) {
this._options = {
get: () => _storage[this._id],
set: (data: T) => (_storage[this._id] = data),
...options,
};
this._cfg = this.get() ?? options.config;
}
/**
* update entire config
* @param data
*/
set(data: T) {
try {
this._options.set(data);
} catch (err) {
console.error('failed to save config', err);
}
this._cfg = data;
}
get(): T;
get(key: keyof T): T[keyof T];
/**
* get config, if key is provided, return the value of the key
* @param key
* @returns
*/
get(key?: keyof T): T | T[keyof T] {
if (!key) {
try {
const cfg = this._options.get();
if (!cfg) {
this.set(this._options.config);
return this._options.config;
}
return cfg;
} catch (err) {
return this._cfg;
}
} else {
return this.get()[key];
}
}
/**
* update a key in config
* @param key
* @param value
*/
patch(key: keyof T, value: any) {
this.set({ ...this.get(), [key]: value });
}
get value(): T {
return this.get();
}
}
export class AppConfigStorage extends Storage<AppConfigSchema> {}

View File

@@ -1,5 +1,6 @@
import type {
ClipboardHandlers,
ConfigStorageHandlers,
DBHandlers,
DebugHandlers,
DialogHandlers,
@@ -49,3 +50,8 @@ export abstract class WorkspaceHandlerManager extends HandlerManager<
'workspace',
WorkspaceHandlers
> {}
export abstract class ConfigStorageHandlerManager extends HandlerManager<
'configStorage',
ConfigStorageHandlers
> {}

View File

@@ -3,6 +3,7 @@ import type Buffer from 'buffer';
import type { WritableAtom } from 'jotai';
import { z } from 'zod';
import type { AppConfigSchema } from './app-config-storage.js';
import type { TypedEventEmitter } from './core/event-emitter.js';
type Buffer = Buffer.Buffer;
@@ -175,6 +176,7 @@ export type UIHandlers = {
handleCloseApp: () => Promise<any>;
getGoogleOauthCode: () => Promise<any>;
getChallengeResponse: (resource: string) => Promise<string>;
handleOpenMainApp: () => Promise<any>;
};
export type ClipboardHandlers = {
@@ -211,6 +213,11 @@ export type WorkspaceHandlers = {
clone: (id: string, newId: string) => Promise<void>;
};
export type ConfigStorageHandlers = {
set: (config: AppConfigSchema | Partial<AppConfigSchema>) => Promise<void>;
get: () => Promise<AppConfigSchema>;
};
export type UnwrapManagerHandlerToServerSide<
ElectronEvent extends {
frameId: number;