@playkit-js/unisphere-service 0.0.17-canary.0-e5f111c
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/LICENSE +661 -0
- package/README.md +99 -0
- package/dist/index.d.ts +40 -0
- package/dist/playkit-unisphere-service.js +3 -0
- package/dist/playkit-unisphere-service.js.LICENSE.txt +1 -0
- package/dist/playkit-unisphere-service.js.map +1 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/package.json +100 -0
- package/src/components/close-button/close-button.tsx +25 -0
- package/src/components/close-button/index.ts +1 -0
- package/src/components/container-element/container-element.tsx +13 -0
- package/src/components/container-element/index.ts +1 -0
- package/src/components/index.ts +4 -0
- package/src/components/sidepanel-buttons-container/index.ts +1 -0
- package/src/components/sidepanel-buttons-container/sidepanel-buttons-container.tsx +40 -0
- package/src/components/upper-bar-icon/index.ts +1 -0
- package/src/components/upper-bar-icon/upper-bar-icon.tsx +23 -0
- package/src/index.ts +6 -0
- package/src/services/container/container-manager.ts +25 -0
- package/src/services/container/index.ts +1 -0
- package/src/services/container/producers/base-container-producer.ts +37 -0
- package/src/services/container/producers/container-producer.ts +8 -0
- package/src/services/container/producers/index.ts +3 -0
- package/src/services/container/producers/sidepanel-container-producer.scss +59 -0
- package/src/services/container/producers/sidepanel-container-producer.tsx +67 -0
- package/src/services/container/producers/simple-container-producer.tsx +27 -0
- package/src/services/index.ts +3 -0
- package/src/services/unisphere/index.ts +4 -0
- package/src/services/unisphere/unisphere-base-service.ts +35 -0
- package/src/services/unisphere/unisphere-element-service.ts +25 -0
- package/src/services/unisphere/unisphere-event-service.ts +36 -0
- package/src/services/unisphere/unisphere-runtime-service.ts +17 -0
- package/src/services/unisphere/unisphere-storage-service.ts +42 -0
- package/src/services/unisphere/unisphere-workspace-service.ts +25 -0
- package/src/services/utils/index.ts +1 -0
- package/src/services/utils/utils.ts +9 -0
- package/src/types/container-types.ts +6 -0
- package/src/types/global.d.ts +1 -0
- package/src/types/index.ts +5 -0
- package/src/types/integrations.ts +38 -0
- package/src/types/service-keys.ts +6 -0
- package/src/types/unisphere-event.ts +3 -0
- package/src/types/unisphere-plugin-config.ts +38 -0
- package/src/types/unisphere-storage-keys.ts +9 -0
- package/src/unisphere-plugin-manager.tsx +254 -0
- package/src/unisphere-plugin.ts +45 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { UnisphereWorkspaceType } from '@unisphere/runtime';
|
|
2
|
+
|
|
3
|
+
class UnisphereWorkspaceService {
|
|
4
|
+
private static workspaceInstance: UnisphereWorkspaceType | null = null;
|
|
5
|
+
|
|
6
|
+
public static set(workspaceInstance: UnisphereWorkspaceType): void {
|
|
7
|
+
UnisphereWorkspaceService.workspaceInstance = workspaceInstance;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public static get(): UnisphereWorkspaceType | null {
|
|
11
|
+
return UnisphereWorkspaceService.workspaceInstance;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public static destroy(): void {
|
|
15
|
+
try {
|
|
16
|
+
(this.workspaceInstance as any).kill();
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// TODO add logs
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
UnisphereWorkspaceService.workspaceInstance = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { UnisphereWorkspaceService };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './utils';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '*.scss';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface UnisphereIntegrationButton {
|
|
2
|
+
id: string;
|
|
3
|
+
icon: string;
|
|
4
|
+
tooltip: string;
|
|
5
|
+
visible?: boolean;
|
|
6
|
+
onClick?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UnisphereIntegrationHeader {
|
|
10
|
+
type: string;
|
|
11
|
+
title: string;
|
|
12
|
+
showCloseButton: boolean;
|
|
13
|
+
buttons: UnisphereIntegrationButton[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UnisphereIntegrationSidepanel {
|
|
17
|
+
header: UnisphereIntegrationHeader;
|
|
18
|
+
visualType: string;
|
|
19
|
+
visualSettings: Record<string, any>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UnisphereIntegrationLayoutHeader {
|
|
23
|
+
icon: string;
|
|
24
|
+
tooltip: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UnisphereIntegrationLayout {
|
|
28
|
+
presets: string[];
|
|
29
|
+
type: string;
|
|
30
|
+
header: UnisphereIntegrationLayoutHeader;
|
|
31
|
+
sidepanel: UnisphereIntegrationSidepanel;
|
|
32
|
+
mountWhileHidden: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UnisphereIntegrations {
|
|
36
|
+
schema: number;
|
|
37
|
+
layouts: UnisphereIntegrationLayout[];
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ReservedPresetAreas, ReservedPresetNames } from 'services/preset-manager/models/preset-item-data';
|
|
2
|
+
import { UnisphereIntegrationLayout } from './integrations';
|
|
3
|
+
|
|
4
|
+
export const isReservedPresetNames = (value: any): value is ReservedPresetNames => {
|
|
5
|
+
return ['Playback'].includes(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ContainerConfig {
|
|
9
|
+
containerType: string;
|
|
10
|
+
presets: ReservedPresetNames[];
|
|
11
|
+
unisphereArea: string;
|
|
12
|
+
position?: string,
|
|
13
|
+
expandMode?: string
|
|
14
|
+
expandOnFirstPlay?: boolean,
|
|
15
|
+
layout?: UnisphereIntegrationLayout
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SidePanelContainerConfig extends ContainerConfig {
|
|
19
|
+
position?: string;
|
|
20
|
+
expandMode?: string;
|
|
21
|
+
expandOnFirstPlay?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SimpleContainerConfig extends ContainerConfig {
|
|
25
|
+
playerArea: ReservedPresetAreas;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WidgetConfig {
|
|
29
|
+
widgetId: string;
|
|
30
|
+
context: string;
|
|
31
|
+
settings: unknown;
|
|
32
|
+
containers: ContainerConfig[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UnispherePluginConfig {
|
|
36
|
+
workspaceName?: string;
|
|
37
|
+
widgets: WidgetConfig[];
|
|
38
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2
|
+
import { h } from 'preact';
|
|
3
|
+
|
|
4
|
+
import { core, KalturaPlayer } from '@playkit-js/kaltura-player-js';
|
|
5
|
+
import { SidePanelsManager, UpperBarManager } from '@playkit-js/ui-managers';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ContainerConfig,
|
|
9
|
+
isReservedPresetNames,
|
|
10
|
+
SidePanelContainerConfig,
|
|
11
|
+
ContainerTypes,
|
|
12
|
+
UnisphereEvent,
|
|
13
|
+
ServiceKeys,
|
|
14
|
+
UnisphereStorageKeys
|
|
15
|
+
} from './types';
|
|
16
|
+
import { UpperBarIcon } from './components';
|
|
17
|
+
import { ContainerManager, getUniqueId, UnisphereEventService, UnisphereStorageService, UnisphereWorkspaceService } from './services';
|
|
18
|
+
import { UnisphereRuntimeBaseType, UnisphereWorkspaceType } from '@unisphere/runtime';
|
|
19
|
+
import { fetchAndLoadUnisphereWorkspace, getUnisphereInstance } from '@unisphere/runtime-js';
|
|
20
|
+
import { UnisphereIntegrations } from './types/integrations';
|
|
21
|
+
|
|
22
|
+
class UnispherePluginManager {
|
|
23
|
+
private iconIds = new Map<string, number>();
|
|
24
|
+
private panelIds = new Set<number>();
|
|
25
|
+
private persistentIconIds = new Map<string, number>();
|
|
26
|
+
private containerManager: ContainerManager;
|
|
27
|
+
private unsubscribeStore?: () => void;
|
|
28
|
+
|
|
29
|
+
private workspaceName = '';
|
|
30
|
+
private shouldCreateWorkspace = false;
|
|
31
|
+
private expandOnFirstPlayPanels = new Set<number>();
|
|
32
|
+
|
|
33
|
+
constructor(private player: KalturaPlayer, private eventManager: core.EventManager, private logger: any, private config: any) {
|
|
34
|
+
this.containerManager = new ContainerManager(player);
|
|
35
|
+
// TODO remove once this is added to providers.env
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
const { unisphereLoaderUrl } = this.player.config.provider;
|
|
39
|
+
if (unisphereLoaderUrl && typeof this.config.workspaceName === 'string') {
|
|
40
|
+
this.workspaceName = this.config.workspaceName;
|
|
41
|
+
} else {
|
|
42
|
+
this.workspaceName = getUniqueId('kaltura_player_workspace_');
|
|
43
|
+
this.shouldCreateWorkspace = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private get upperBarManager(): UpperBarManager | undefined {
|
|
48
|
+
return this.player.getService(ServiceKeys.UPPER_BAR_MANAGER) as UpperBarManager;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private get sidePanelsManager(): SidePanelsManager | undefined {
|
|
52
|
+
return this.player.getService(ServiceKeys.SIDE_PANELS_MANAGER) as SidePanelsManager;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private addEventHandlers(): void {
|
|
56
|
+
// listen to events from runtime side
|
|
57
|
+
UnisphereEventService.subscribe(UnisphereEvent.MEDIA_SEEK_TO_TIME, (data) => {
|
|
58
|
+
const { seekToInSeconds, entryId, play } = data.payload as {
|
|
59
|
+
seekToInSeconds: string;
|
|
60
|
+
entryId?: string;
|
|
61
|
+
play?: boolean;
|
|
62
|
+
};
|
|
63
|
+
this.handleSeekToTime({ seekToInSeconds, entryId, play });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// listen to player events
|
|
67
|
+
this.eventManager.listenOnce(this.player, core.EventType.FIRST_PLAYING, () => {
|
|
68
|
+
this.handleFirstPlay();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private handleFirstPlay(): void {
|
|
73
|
+
// activate all panels that should expand on first play
|
|
74
|
+
for (const panelId of this.expandOnFirstPlayPanels) {
|
|
75
|
+
this.sidePanelsManager?.activateItem(panelId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private handleSeekToTime(data: { seekToInSeconds: string; entryId?: string; play?: boolean }): void {
|
|
80
|
+
const { seekToInSeconds, entryId, play } = data;
|
|
81
|
+
const seekTime = parseFloat(seekToInSeconds);
|
|
82
|
+
|
|
83
|
+
if (isNaN(seekTime)) {
|
|
84
|
+
this.logger.warn('Invalid seekToInSeconds value:', seekToInSeconds);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (entryId && this.player.sources.id !== entryId) {
|
|
89
|
+
this.logger.warn('Entry ID mismatch:', { current: this.player.sources.id, requested: entryId });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.player.currentTime = seekTime;
|
|
94
|
+
|
|
95
|
+
if (play === true) {
|
|
96
|
+
this.player.play();
|
|
97
|
+
} else if (play === false) {
|
|
98
|
+
this.player.pause();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async getWorkspace(): Promise<UnisphereWorkspaceType> {
|
|
103
|
+
let workspace: UnisphereWorkspaceType;
|
|
104
|
+
|
|
105
|
+
if (this.shouldCreateWorkspace) {
|
|
106
|
+
workspace = (await fetchAndLoadUnisphereWorkspace({
|
|
107
|
+
// TODO remove once this is added to providers.env
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
serverUrl: this.player.config.provider.unisphereLoaderUrl,
|
|
111
|
+
workspaceName: this.workspaceName,
|
|
112
|
+
appId: 'kaltura-player',
|
|
113
|
+
appVersion: '',
|
|
114
|
+
ui: {
|
|
115
|
+
theme: 'light',
|
|
116
|
+
language: 'en'
|
|
117
|
+
}
|
|
118
|
+
})) as unknown as UnisphereWorkspaceType; // TODO remove once there the type is updated
|
|
119
|
+
} else {
|
|
120
|
+
workspace = await getUnisphereInstance(this.config.workspaceName);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
UnisphereWorkspaceService.set(workspace);
|
|
124
|
+
return workspace;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private addUpperBarIcon(id: string, label: string, order: number, iconPath: string, panelId: number, isPersistent: boolean): void {
|
|
128
|
+
if (this.iconIds.has(id) || this.persistentIconIds.has(id)) return;
|
|
129
|
+
|
|
130
|
+
this.panelIds.add(panelId);
|
|
131
|
+
const iconId = this.upperBarManager?.add({
|
|
132
|
+
displayName: label,
|
|
133
|
+
ariaLabel: label,
|
|
134
|
+
order,
|
|
135
|
+
component: () => (<UpperBarIcon id={id} label={label} iconPath={iconPath} />) as preact.JSX.Element,
|
|
136
|
+
svgIcon: { path: iconPath },
|
|
137
|
+
onClick: () => {
|
|
138
|
+
if (this.sidePanelsManager?.isItemActive(panelId)) {
|
|
139
|
+
this.sidePanelsManager?.deactivateItem(panelId);
|
|
140
|
+
} else {
|
|
141
|
+
this.sidePanelsManager?.activateItem(panelId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}) as number;
|
|
145
|
+
|
|
146
|
+
if (isPersistent) {
|
|
147
|
+
this.persistentIconIds.set(id, iconId);
|
|
148
|
+
} else {
|
|
149
|
+
this.iconIds.set(id, iconId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public initRuntime(runtime: UnisphereRuntimeBaseType<any>, integrations: UnisphereIntegrations, sidePanelConfig?: SidePanelContainerConfig): void {
|
|
154
|
+
if (!runtime?.widgetName || !runtime?.runtimeName || !integrations?.layouts) return;
|
|
155
|
+
|
|
156
|
+
this.addEventHandlers();
|
|
157
|
+
|
|
158
|
+
const layouts = integrations.layouts;
|
|
159
|
+
|
|
160
|
+
if (layouts && layouts.length > 0) {
|
|
161
|
+
const currentPreset = this.player.ui.store.getState().shell.activePresetName;
|
|
162
|
+
|
|
163
|
+
for (const layout of layouts) {
|
|
164
|
+
if (layout?.type === ContainerTypes.SIDEPANEL && layout?.presets?.includes(currentPreset)) {
|
|
165
|
+
const containerConfig: ContainerConfig = {
|
|
166
|
+
containerType: ContainerTypes.SIDEPANEL,
|
|
167
|
+
presets: layout.presets.filter(isReservedPresetNames),
|
|
168
|
+
unisphereArea: layout.sidepanel.visualType,
|
|
169
|
+
position: sidePanelConfig?.position,
|
|
170
|
+
expandMode: sidePanelConfig?.expandMode,
|
|
171
|
+
expandOnFirstPlay: sidePanelConfig?.expandOnFirstPlay,
|
|
172
|
+
layout
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const containerId = this.containerManager.createContainer(containerConfig);
|
|
176
|
+
if (containerId) {
|
|
177
|
+
const storageKey = layout.sidepanel.visualType;
|
|
178
|
+
const panelData = UnisphereStorageService.get(storageKey) as any;
|
|
179
|
+
const panelId = panelData?.upperBarIcon?.panelId;
|
|
180
|
+
|
|
181
|
+
if (panelId && layout.header) {
|
|
182
|
+
this.addUpperBarIcon(getUniqueId(runtime.widgetName.split('.')[2]), layout.header.tooltip, 0, layout.header.icon, panelId, false);
|
|
183
|
+
}
|
|
184
|
+
this.mountRuntimeToContainer(runtime, layout, containerId);
|
|
185
|
+
|
|
186
|
+
// track panels that should expand on first play
|
|
187
|
+
if (sidePanelConfig?.expandOnFirstPlay && panelId) {
|
|
188
|
+
this.expandOnFirstPlayPanels.add(panelId);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private mountRuntimeToContainer(runtimeInstance: any, layout: any, containerId: string): void {
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
const targetElement = document.getElementById(containerId);
|
|
199
|
+
if (targetElement) {
|
|
200
|
+
runtimeInstance?.mountVisual({
|
|
201
|
+
type: layout.sidepanel.visualType,
|
|
202
|
+
settings: layout.sidepanel.visualSettings,
|
|
203
|
+
target: targetElement
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}, 100);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public updateSessionData(): void {
|
|
210
|
+
UnisphereStorageService.update(UnisphereStorageKeys.PLAYER_SESSION, {
|
|
211
|
+
entryId: this.player.sources.id,
|
|
212
|
+
ks: this.player.config.session?.ks,
|
|
213
|
+
entryName: this.player.sources.metadata?.name
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public reset(): void {
|
|
218
|
+
for (const panelId of this.panelIds) {
|
|
219
|
+
if (this.sidePanelsManager?.isItemActive(panelId)) {
|
|
220
|
+
this.sidePanelsManager.deactivateItem(panelId);
|
|
221
|
+
}
|
|
222
|
+
this.sidePanelsManager?.remove(panelId);
|
|
223
|
+
}
|
|
224
|
+
this.panelIds.clear();
|
|
225
|
+
|
|
226
|
+
for (const v of this.iconIds.values()) {
|
|
227
|
+
this.upperBarManager?.remove(v);
|
|
228
|
+
}
|
|
229
|
+
this.iconIds.clear();
|
|
230
|
+
this.containerManager.destroy();
|
|
231
|
+
|
|
232
|
+
// reset expandOnFirstPlay state
|
|
233
|
+
this.expandOnFirstPlayPanels.clear();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public destroy(): void {
|
|
237
|
+
this.reset();
|
|
238
|
+
this.containerManager.destroy();
|
|
239
|
+
|
|
240
|
+
for (const v of this.persistentIconIds.values()) {
|
|
241
|
+
this.upperBarManager?.remove(v);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
UnisphereEventService.destroy();
|
|
245
|
+
UnisphereStorageService.destroy();
|
|
246
|
+
UnisphereWorkspaceService.destroy();
|
|
247
|
+
|
|
248
|
+
if (this.unsubscribeStore) {
|
|
249
|
+
this.unsubscribeStore();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export { UnispherePluginManager };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { BasePlugin, KalturaPlayer } from '@playkit-js/kaltura-player-js';
|
|
2
|
+
import { UnispherePluginConfig } from './types';
|
|
3
|
+
import { UnispherePluginManager } from './unisphere-plugin-manager';
|
|
4
|
+
import { UnisphereEventService } from './services';
|
|
5
|
+
|
|
6
|
+
export const PLUGIN_NAME = 'unisphereService';
|
|
7
|
+
|
|
8
|
+
export class UnispherePlugin extends BasePlugin {
|
|
9
|
+
private _unispherePluginManager: UnispherePluginManager;
|
|
10
|
+
/**
|
|
11
|
+
* The default configuration of the plugin.
|
|
12
|
+
* @static
|
|
13
|
+
*/
|
|
14
|
+
public static defaultConfig: UnispherePluginConfig = {
|
|
15
|
+
widgets: []
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
constructor(name: string, player: KalturaPlayer, config: UnispherePluginConfig) {
|
|
19
|
+
super(name, player, config);
|
|
20
|
+
this._unispherePluginManager = new UnispherePluginManager(player, this.eventManager, this.logger, this.config);
|
|
21
|
+
player.registerService(PLUGIN_NAME, {
|
|
22
|
+
pluginManager: this._unispherePluginManager,
|
|
23
|
+
eventService: UnisphereEventService
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public reset(): void {
|
|
28
|
+
this._unispherePluginManager.reset();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public destroy(): void {
|
|
32
|
+
this.eventManager.destroy();
|
|
33
|
+
this._unispherePluginManager.reset();
|
|
34
|
+
UnisphereEventService.destroy();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @static
|
|
39
|
+
* @public
|
|
40
|
+
* @returns {boolean} - Whether the plugin is valid.
|
|
41
|
+
*/
|
|
42
|
+
public static isValid(): boolean {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|