@netless/window-manager 0.4.0-canary.11 → 0.4.0-canary.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/App/MagixEvent/index.d.ts +2 -1
- package/dist/App/Storage/index.d.ts +2 -1
- package/dist/App/Storage/typings.d.ts +1 -0
- package/dist/AppManager.d.ts +2 -1
- package/dist/AppProxy.d.ts +1 -0
- package/dist/Utils/Common.d.ts +3 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/docs/api.md +8 -6
- package/docs/concept.md +4 -0
- package/package.json +2 -2
- package/src/App/MagixEvent/index.ts +3 -1
- package/src/App/Storage/index.ts +6 -1
- package/src/App/Storage/typings.ts +4 -2
- package/src/AppContext.ts +7 -2
- package/src/AppManager.ts +40 -19
- package/src/AppProxy.ts +24 -11
- package/src/Utils/Common.ts +22 -1
- package/src/Utils/RoomHacker.ts +1 -0
- package/src/View/ViewManager.ts +1 -2
- package/src/index.ts +15 -1
package/docs/api.md
CHANGED
@@ -107,12 +107,13 @@ manager.closeApp(appId)
|
|
107
107
|
|
108
108
|
<h2 id="prototypes">实例属性</h2>
|
109
109
|
|
110
|
-
| name | type | default | desc
|
111
|
-
| ------------------ | ------- | ------- |
|
112
|
-
| mainView | View | | 主白板
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
| name | type | default | desc |
|
111
|
+
| ------------------ | ------- | ------- | ----------------- |
|
112
|
+
| mainView | View | | 主白板 |
|
113
|
+
| mainViewSceneIndex | number | | 当前主白板的 SceneIndex |
|
114
|
+
| boxState | string | | 当前窗口状态 |
|
115
|
+
| darkMode | boolean | | 黑夜模式 |
|
116
|
+
| prefersColorScheme | string | | 颜色主题 |
|
116
117
|
|
117
118
|
|
118
119
|
<h2 id="events">事件回调</h2>
|
@@ -124,6 +125,7 @@ manager.callbacks.on(events, listener)
|
|
124
125
|
| name | type | default | desc |
|
125
126
|
| ------------------------ | -------------- | ------- | -------------------------- |
|
126
127
|
| mainViewModeChange | ViewVisionMode | | |
|
128
|
+
| mainViewSceneIndexChange | index: number | | |
|
127
129
|
| boxStateChange | string | | normal,minimized,maximized |
|
128
130
|
| darkModeChange | boolean | | |
|
129
131
|
| prefersColorSchemeChange | string | | auto,light,dark |
|
package/docs/concept.md
ADDED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@netless/window-manager",
|
3
|
-
"version": "0.4.0-canary.
|
3
|
+
"version": "0.4.0-canary.15",
|
4
4
|
"description": "",
|
5
5
|
"main": "dist/index.es.js",
|
6
6
|
"module": "dist/index.es.js",
|
@@ -19,7 +19,7 @@
|
|
19
19
|
"license": "ISC",
|
20
20
|
"peerDependencies": {
|
21
21
|
"video.js": ">=7",
|
22
|
-
"white-web-sdk": "^2.
|
22
|
+
"white-web-sdk": "^2.16.0"
|
23
23
|
},
|
24
24
|
"dependencies": {
|
25
25
|
"@juggle/resize-observer": "^3.3.1",
|
@@ -50,13 +50,15 @@ export type MagixEventHandler<
|
|
50
50
|
TEvent extends MagixEventTypes<TPayloads> = MagixEventTypes<TPayloads>
|
51
51
|
> = (message: MagixEventMessage<TPayloads, TEvent>) => void;
|
52
52
|
|
53
|
+
export type MagixEventListenerDisposer = () => void
|
54
|
+
|
53
55
|
export type MagixEventAddListener<TPayloads = any> = <
|
54
56
|
TEvent extends MagixEventTypes<TPayloads> = MagixEventTypes<TPayloads>
|
55
57
|
>(
|
56
58
|
event: TEvent,
|
57
59
|
handler: MagixEventHandler<TPayloads, TEvent>,
|
58
60
|
options?: MagixEventListenerOptions | undefined
|
59
|
-
) =>
|
61
|
+
) => MagixEventListenerDisposer;
|
60
62
|
|
61
63
|
export type MagixEventRemoveListener<TPayloads = any> = <
|
62
64
|
TEvent extends MagixEventTypes<TPayloads> = MagixEventTypes<TPayloads>
|
package/src/App/Storage/index.ts
CHANGED
@@ -4,7 +4,7 @@ import { SideEffectManager } from "side-effect-manager";
|
|
4
4
|
import type { AppContext } from "../../AppContext";
|
5
5
|
import { safeListenPropsUpdated } from "../../Utils/Reactive";
|
6
6
|
import { isRef, makeRef, plainObjectKeys } from "./utils";
|
7
|
-
import type { Diff, MaybeRefValue, RefValue, StorageStateChangedEvent } from "./typings";
|
7
|
+
import type { Diff, MaybeRefValue, RefValue, StorageStateChangedEvent, StorageStateChangedListener, StorageStateChangedListenerDisposer } from "./typings";
|
8
8
|
import { StorageEvent } from "./StorageEvent";
|
9
9
|
|
10
10
|
export * from './typings';
|
@@ -86,6 +86,11 @@ export class Storage<TState extends Record<string, any> = any> implements Storag
|
|
86
86
|
}
|
87
87
|
|
88
88
|
readonly onStateChanged = new StorageEvent<StorageStateChangedEvent<TState>>();
|
89
|
+
|
90
|
+
addStateChangedListener(handler: StorageStateChangedListener<TState>): StorageStateChangedListenerDisposer {
|
91
|
+
this.onStateChanged.addListener(handler);
|
92
|
+
return () => this.onStateChanged.removeListener(handler);
|
93
|
+
}
|
89
94
|
|
90
95
|
ensureState(state: Partial<TState>): void {
|
91
96
|
return this.setState(
|
@@ -16,6 +16,8 @@ export type StorageOnSetStatePayload<TState = unknown> = {
|
|
16
16
|
[K in keyof TState]?: MaybeRefValue<TState[K]>;
|
17
17
|
};
|
18
18
|
|
19
|
-
export type StorageStateChangedEvent<TState = any> = Diff<TState
|
19
|
+
export type StorageStateChangedEvent<TState = any> = Diff<TState>;
|
20
20
|
|
21
|
-
export type StorageStateChangedListener<TState = any> = StorageEventListener<StorageStateChangedEvent<TState
|
21
|
+
export type StorageStateChangedListener<TState = any> = StorageEventListener<StorageStateChangedEvent<TState>>;
|
22
|
+
|
23
|
+
export type StorageStateChangedListenerDisposer = () => void;
|
package/src/AppContext.ts
CHANGED
@@ -8,7 +8,7 @@ import {
|
|
8
8
|
toJS
|
9
9
|
} from 'white-web-sdk';
|
10
10
|
import { BoxNotCreatedError } from './Utils/error';
|
11
|
-
import type { Room, SceneDefinition, View } from "white-web-sdk";
|
11
|
+
import type { Room, SceneDefinition, View, EventListener as WhiteEventListener } from "white-web-sdk";
|
12
12
|
import type { ReadonlyTeleBox } from "@netless/telebox-insider";
|
13
13
|
import type Emittery from "emittery";
|
14
14
|
import type { BoxManager } from "./BoxManager";
|
@@ -110,6 +110,8 @@ export class AppContext<TAttributes = any, TMagixEventPayloads = any, TAppOption
|
|
110
110
|
public setScenePath = async (scenePath: string): Promise<void> => {
|
111
111
|
if (!this.appProxy.box) return;
|
112
112
|
this.appProxy.setFullPath(scenePath);
|
113
|
+
// 兼容 15 版本 SDK 的切页
|
114
|
+
this.getRoom()?.setScenePath(scenePath);
|
113
115
|
}
|
114
116
|
|
115
117
|
public mountView = (dom: HTMLDivElement): void => {
|
@@ -156,7 +158,10 @@ export class AppContext<TAttributes = any, TMagixEventPayloads = any, TAppOption
|
|
156
158
|
public dispatchMagixEvent: MagixEventDispatcher<TMagixEventPayloads> = (this.manager.displayer as Room).dispatchMagixEvent.bind(this.manager.displayer)
|
157
159
|
|
158
160
|
/** Listen to events from others clients (and self messages). */
|
159
|
-
public addMagixEventListener: MagixEventAddListener<TMagixEventPayloads> =
|
161
|
+
public addMagixEventListener: MagixEventAddListener<TMagixEventPayloads> = (event, handler, options) => {
|
162
|
+
this.manager.displayer.addMagixEventListener(event, handler as WhiteEventListener, options);
|
163
|
+
return () => this.manager.displayer.removeMagixEventListener(event, handler as WhiteEventListener);
|
164
|
+
}
|
160
165
|
|
161
166
|
/** Remove a Magix event listener. */
|
162
167
|
public removeMagixEventListener = this.manager.displayer.removeMagixEventListener.bind(this.manager.displayer) as MagixEventRemoveListener<TMagixEventPayloads>
|
package/src/AppManager.ts
CHANGED
@@ -4,11 +4,11 @@ import { AppListeners } from "./AppListener";
|
|
4
4
|
import { AppProxy } from "./AppProxy";
|
5
5
|
import { autorun, isPlayer, isRoom, ScenePathType } from "white-web-sdk";
|
6
6
|
import { callbacks, emitter, WindowManager, reconnectRefresher } from "./index";
|
7
|
-
import { genAppId, makeValidScenePath, setScenePath, setViewFocusScenePath } from "./Utils/Common";
|
7
|
+
import { entireScenes, genAppId, makeValidScenePath, parseSceneDir, setScenePath, setViewFocusScenePath } from "./Utils/Common";
|
8
8
|
import { log } from "./Utils/log";
|
9
9
|
import { MainViewProxy } from "./View/MainView";
|
10
10
|
import { onObjectRemoved, safeListenPropsUpdated } from "./Utils/Reactive";
|
11
|
-
import { get, sortBy } from "lodash";
|
11
|
+
import { get, isInteger, sortBy } from "lodash";
|
12
12
|
import { store } from "./AttributesDelegate";
|
13
13
|
import { ViewManager } from "./View/ViewManager";
|
14
14
|
import type { ReconnectRefresher } from "./ReconnectRefresher";
|
@@ -212,10 +212,11 @@ export class AppManager {
|
|
212
212
|
emitter.emit("mainViewMounted");
|
213
213
|
}
|
214
214
|
|
215
|
-
public setMainViewFocusPath() {
|
216
|
-
const
|
217
|
-
if (
|
218
|
-
setViewFocusScenePath(this.mainView,
|
215
|
+
public setMainViewFocusPath(scenePath?: string) {
|
216
|
+
const focusScenePath = scenePath || this.store.getMainViewScenePath();
|
217
|
+
if (focusScenePath) {
|
218
|
+
const view = setViewFocusScenePath(this.mainView, focusScenePath);
|
219
|
+
return view?.focusScenePath === focusScenePath;
|
219
220
|
}
|
220
221
|
}
|
221
222
|
|
@@ -318,6 +319,9 @@ export class AppManager {
|
|
318
319
|
});
|
319
320
|
if (isWritable === true) {
|
320
321
|
this.mainView.disableCameraTransform = false;
|
322
|
+
if (this.room && this.room.disableSerialization === true) {
|
323
|
+
this.room.disableSerialization = false;
|
324
|
+
}
|
321
325
|
} else {
|
322
326
|
this.mainView.disableCameraTransform = true;
|
323
327
|
}
|
@@ -374,27 +378,44 @@ export class AppManager {
|
|
374
378
|
}
|
375
379
|
|
376
380
|
private async _setMainViewScenePath(scenePath: string) {
|
377
|
-
this.
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
+
const success = this.setMainViewFocusPath(scenePath);
|
382
|
+
if (success) {
|
383
|
+
this.safeSetAttributes({ _mainScenePath: scenePath });
|
384
|
+
this.store.setMainViewFocusPath(this.mainView);
|
385
|
+
this.updateSceneIndex();
|
386
|
+
this.dispatchInternalEvent(Events.SetMainViewScenePath, { nextScenePath: scenePath });
|
387
|
+
}
|
388
|
+
}
|
389
|
+
|
390
|
+
private updateSceneIndex = () => {
|
391
|
+
const scenePath = this.store.getMainViewScenePath() as string;
|
392
|
+
const sceneDir = parseSceneDir(scenePath);
|
393
|
+
const scenes = entireScenes(this.displayer)[sceneDir];
|
394
|
+
if (scenes.length) {
|
395
|
+
// "/ppt3/1" -> "1"
|
396
|
+
const pageName = scenePath.replace(sceneDir, "").replace("/", "");
|
397
|
+
const index = scenes.findIndex(scene => scene.name === pageName);
|
398
|
+
if (isInteger(index) && index >= 0) {
|
399
|
+
this.safeSetAttributes({ _mainSceneIndex: index });
|
400
|
+
}
|
401
|
+
}
|
381
402
|
}
|
382
403
|
|
383
404
|
public async setMainViewSceneIndex(index: number) {
|
384
405
|
if (this.room) {
|
385
|
-
this.
|
406
|
+
if (this.store.getMainViewSceneIndex() === index) return;
|
386
407
|
const mainViewScenePath = this.store.getMainViewScenePath() as string;
|
387
408
|
if (mainViewScenePath) {
|
388
|
-
const
|
389
|
-
sceneList.pop();
|
390
|
-
let sceneDir = sceneList.join("/");
|
391
|
-
if (sceneDir === "") {
|
392
|
-
sceneDir = "/";
|
393
|
-
}
|
409
|
+
const sceneDir = parseSceneDir(mainViewScenePath);
|
394
410
|
const scenePath = makeValidScenePath(this.displayer, sceneDir, index);
|
395
411
|
if (scenePath) {
|
396
|
-
this.
|
397
|
-
|
412
|
+
const success = this.setMainViewFocusPath(scenePath);
|
413
|
+
if (success) {
|
414
|
+
this.store.setMainViewScenePath(scenePath);
|
415
|
+
this.safeSetAttributes({ _mainSceneIndex: index });
|
416
|
+
}
|
417
|
+
} else {
|
418
|
+
throw new Error(`[WindowManager]: ${sceneDir}: ${index} not valid index`);
|
398
419
|
}
|
399
420
|
}
|
400
421
|
}
|
package/src/AppProxy.ts
CHANGED
@@ -5,13 +5,9 @@ import { appRegister } from "./Register";
|
|
5
5
|
import { autorun } from "white-web-sdk";
|
6
6
|
import { emitter } from "./index";
|
7
7
|
import { Fields } from "./AttributesDelegate";
|
8
|
-
import { get } from "lodash";
|
8
|
+
import { debounce, get } from "lodash";
|
9
9
|
import { log } from "./Utils/log";
|
10
|
-
import {
|
11
|
-
setScenePath,
|
12
|
-
setViewFocusScenePath,
|
13
|
-
getScenePath
|
14
|
-
} from "./Utils/Common";
|
10
|
+
import { setScenePath, setViewFocusScenePath, getScenePath } from "./Utils/Common";
|
15
11
|
import type {
|
16
12
|
AppEmitterEvent,
|
17
13
|
AppInitState,
|
@@ -113,9 +109,7 @@ export class AppProxy extends Base {
|
|
113
109
|
this.manager.safeUpdateAttributes(["apps", this.id, Fields.FullPath], path);
|
114
110
|
}
|
115
111
|
|
116
|
-
public async baseInsertApp(
|
117
|
-
skipUpdate = false,
|
118
|
-
): Promise<{ appId: string; app: NetlessApp }> {
|
112
|
+
public async baseInsertApp(skipUpdate = false): Promise<{ appId: string; app: NetlessApp }> {
|
119
113
|
const params = this.params;
|
120
114
|
if (!params.kind) {
|
121
115
|
throw new Error("[WindowManager]: kind require");
|
@@ -123,7 +117,13 @@ export class AppProxy extends Base {
|
|
123
117
|
const appImpl = await appRegister.appClasses.get(params.kind)?.();
|
124
118
|
const appParams = appRegister.registered.get(params.kind);
|
125
119
|
if (appImpl) {
|
126
|
-
await this.setupApp(
|
120
|
+
await this.setupApp(
|
121
|
+
this.id,
|
122
|
+
skipUpdate,
|
123
|
+
appImpl,
|
124
|
+
params.options,
|
125
|
+
appParams?.appOptions
|
126
|
+
);
|
127
127
|
} else {
|
128
128
|
throw new Error(`[WindowManager]: app load failed ${params.kind} ${params.src}`);
|
129
129
|
}
|
@@ -317,7 +317,7 @@ export class AppProxy extends Base {
|
|
317
317
|
}
|
318
318
|
});
|
319
319
|
});
|
320
|
-
this.manager.refresher?.add(this.stateKey,() => {
|
320
|
+
this.manager.refresher?.add(this.stateKey, () => {
|
321
321
|
return autorun(() => {
|
322
322
|
const appState = this.appAttributes?.state;
|
323
323
|
if (appState?.zIndex > 0 && appState.zIndex !== this.box?.zIndex) {
|
@@ -325,8 +325,20 @@ export class AppProxy extends Base {
|
|
325
325
|
}
|
326
326
|
});
|
327
327
|
});
|
328
|
+
this.manager.refresher?.add(`${appId}-fullPath`, () => {
|
329
|
+
return autorun(() => {
|
330
|
+
const fullPath = this.appAttributes?.fullPath;
|
331
|
+
this.setFocusScenePathHandler(fullPath);
|
332
|
+
});
|
333
|
+
});
|
328
334
|
};
|
329
335
|
|
336
|
+
private setFocusScenePathHandler = debounce((fullPath: string | undefined) => {
|
337
|
+
if (this.view && fullPath && fullPath !== this.view?.focusScenePath) {
|
338
|
+
setViewFocusScenePath(this.view, fullPath);
|
339
|
+
}
|
340
|
+
}, 50);
|
341
|
+
|
330
342
|
public setScenePath(): void {
|
331
343
|
if (!this.manager.canOperate) return;
|
332
344
|
const fullScenePath = this.getFullScenePath();
|
@@ -372,6 +384,7 @@ export class AppProxy extends Base {
|
|
372
384
|
this.manager.appStatus.delete(this.id);
|
373
385
|
this.manager.refresher?.remove(this.id);
|
374
386
|
this.manager.refresher?.remove(this.stateKey);
|
387
|
+
this.manager.refresher?.remove(`${this.id}-fullPath`);
|
375
388
|
}
|
376
389
|
|
377
390
|
public close(): Promise<void> {
|
package/src/Utils/Common.ts
CHANGED
@@ -17,9 +17,17 @@ export const genAppId = async (kind: string) => {
|
|
17
17
|
export const setViewFocusScenePath = (view: View, focusScenePath: string) => {
|
18
18
|
if (view.focusScenePath !== focusScenePath) {
|
19
19
|
view.focusScenePath = focusScenePath;
|
20
|
+
return view;
|
20
21
|
}
|
21
22
|
};
|
22
23
|
|
24
|
+
export const setViewSceneIndex = (view: View, index: number) => {
|
25
|
+
if (view.focusSceneIndex !== index) {
|
26
|
+
view.focusSceneIndex = index;
|
27
|
+
return view;
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
23
31
|
export const setScenePath = (room: Room | undefined, scenePath: string) => {
|
24
32
|
if (room && room.isWritable) {
|
25
33
|
if (room.state.sceneState.scenePath !== scenePath) {
|
@@ -70,7 +78,9 @@ export const notifyMainViewModeChange = debounce(
|
|
70
78
|
export const makeValidScenePath = (displayer: Displayer, scenePath: string, index = 0) => {
|
71
79
|
const scenes = entireScenes(displayer)[scenePath];
|
72
80
|
if (!scenes) return;
|
73
|
-
const
|
81
|
+
const scene = scenes[index];
|
82
|
+
if (!scene) return;
|
83
|
+
const firstSceneName = scene.name;
|
74
84
|
if (scenePath === "/") {
|
75
85
|
return `/${firstSceneName}`;
|
76
86
|
} else {
|
@@ -86,6 +96,17 @@ export const isValidScenePath = (scenePath: string) => {
|
|
86
96
|
return scenePath.startsWith("/");
|
87
97
|
};
|
88
98
|
|
99
|
+
export const parseSceneDir = (scenePath: string) => {
|
100
|
+
const sceneList = scenePath.split("/");
|
101
|
+
sceneList.pop();
|
102
|
+
let sceneDir = sceneList.join("/");
|
103
|
+
// "/page1" 的 dir 为 "/"
|
104
|
+
if (sceneDir === "") {
|
105
|
+
sceneDir = "/";
|
106
|
+
}
|
107
|
+
return sceneDir;
|
108
|
+
}
|
109
|
+
|
89
110
|
export const ensureValidScenePath = (scenePath: string) => {
|
90
111
|
if (scenePath.endsWith("/")) {
|
91
112
|
return scenePath.slice(0, -1);
|
package/src/Utils/RoomHacker.ts
CHANGED
@@ -51,6 +51,7 @@ export const replaceRoomFunction = (room: Room, manager: WindowManager) => {
|
|
51
51
|
room.setMemberState = (...args) => manager.mainView.setMemberState(...args);
|
52
52
|
room.redo = () => manager.mainView.redo();
|
53
53
|
room.undo = () => manager.mainView.undo();
|
54
|
+
room.cleanCurrentScene = () => manager.mainView.cleanCurrentScene();
|
54
55
|
}
|
55
56
|
|
56
57
|
};
|
package/src/View/ViewManager.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import type { View
|
1
|
+
import type { View, Displayer } from "white-web-sdk";
|
2
2
|
|
3
3
|
export class ViewManager {
|
4
4
|
public views: Map<string, View> = new Map();
|
@@ -38,7 +38,6 @@ export class ViewManager {
|
|
38
38
|
}
|
39
39
|
}
|
40
40
|
|
41
|
-
|
42
41
|
export const createView = (displayer: Displayer): View => {
|
43
42
|
const view = displayer.views.createView();
|
44
43
|
setDefaultCameraBound(view);
|
package/src/index.ts
CHANGED
@@ -21,6 +21,7 @@ import {
|
|
21
21
|
ensureValidScenePath,
|
22
22
|
getVersionNumber,
|
23
23
|
isValidScenePath,
|
24
|
+
parseSceneDir,
|
24
25
|
wait,
|
25
26
|
} from "./Utils/Common";
|
26
27
|
import type { TELE_BOX_STATE, BoxManager } from "./BoxManager";
|
@@ -220,7 +221,7 @@ export class WindowManager extends InvisiblePlugin<WindowMangerAttributes> {
|
|
220
221
|
if (room.phase !== RoomPhase.Connected) {
|
221
222
|
throw new Error("[WindowManager]: Room only Connected can be mount");
|
222
223
|
}
|
223
|
-
if (room.phase === RoomPhase.Connected) {
|
224
|
+
if (room.phase === RoomPhase.Connected && room.isWritable) {
|
224
225
|
// redo undo 需要设置这个属性
|
225
226
|
room.disableSerialization = false;
|
226
227
|
}
|
@@ -571,6 +572,19 @@ export class WindowManager extends InvisiblePlugin<WindowMangerAttributes> {
|
|
571
572
|
return this.attributes.focus;
|
572
573
|
}
|
573
574
|
|
575
|
+
public get mainViewSceneIndex(): number {
|
576
|
+
return this.appManager?.store.getMainViewSceneIndex();
|
577
|
+
}
|
578
|
+
|
579
|
+
public get mainViewSceneDir(): string {
|
580
|
+
const scenePath = this.appManager?.store.getMainViewScenePath();
|
581
|
+
if (scenePath) {
|
582
|
+
return parseSceneDir(scenePath);
|
583
|
+
} else {
|
584
|
+
throw new Error("[WindowManager]: mainViewSceneDir not found");
|
585
|
+
}
|
586
|
+
}
|
587
|
+
|
574
588
|
/**
|
575
589
|
* 查询所有的 App
|
576
590
|
*/
|