@24i/bigscreen-sdk 1.0.7 → 1.0.8-alpha.2177
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/package.json +1 -1
- package/packages/analytics/src/clients/GoogleAnalytics/GoogleAnalytics.ts +135 -34
- package/packages/analytics/src/clients/GoogleAnalytics/constants.ts +22 -9
- package/packages/analytics/src/clients/GoogleAnalytics/generateSessionId.ts +8 -0
- package/packages/analytics/src/clients/GoogleAnalytics/getUrl.ts +17 -0
- package/packages/analytics/src/clients/GoogleAnalytics/index.ts +1 -0
- package/packages/analytics/src/clients/GoogleAnalytics/interface.ts +47 -0
- package/packages/analytics/src/clients/GoogleAnalytics/mapPayload.ts +18 -65
- package/packages/analytics/src/clients/GoogleAnalytics/prepareBody.ts +14 -0
- package/packages/grid/src/FirstOnlyGrid/FirstOnlyGrid.tsx +4 -2
- package/packages/keyboard/src/Backdrop/Backdrop.scss +2 -0
- package/packages/keyboard/src/WhitelabelKeyboard/Keyboard.tsx +6 -1
- package/packages/player-ui/src/PlayerUI.tsx +6 -2
- package/packages/player-ui/src/Seekbar.tsx +46 -0
- package/packages/player-ui/src/Seeking.ts +1 -0
- package/packages/player-ui/src/mocks.ts +2 -0
- package/packages/player-ui/src/types.ts +2 -0
- package/packages/utils/src/textUtils.ts +47 -13
package/package.json
CHANGED
|
@@ -1,59 +1,143 @@
|
|
|
1
1
|
import { Storage } from '@24i/bigscreen-sdk/storage';
|
|
2
|
+
import { device } from '@24i/bigscreen-sdk/device';
|
|
2
3
|
import { debounce } from '@24i/bigscreen-sdk/utils/debounce';
|
|
4
|
+
import { createTimeout } from '@24i/bigscreen-sdk/utils/timers';
|
|
5
|
+
import { MINUTE_IN_MS } from '@24i/bigscreen-sdk/utils/timeConstants';
|
|
3
6
|
import { generateUuid } from '@24i/bigscreen-sdk/utils/generateUuid';
|
|
4
|
-
import type { AnalyticsClient, AnalyticsEventsMap } from '../../interface';
|
|
5
|
-
import { mapPayload } from './mapPayload';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import type { AnalyticsClient, AnalyticsEventsMap, SceneInfo } from '../../interface';
|
|
8
|
+
import { mapEventParamsToUrlParams, mapPayload } from './mapPayload';
|
|
9
|
+
import { prepareBody } from './prepareBody';
|
|
10
|
+
import { getUrl } from './getUrl';
|
|
11
|
+
import { generateSessionId } from './generateSessionId';
|
|
12
|
+
import { GOOGLE_ANALYTICS_TRIGGERS, TRIGGER_NAME_TO_GA_EVENTS } from './constants';
|
|
13
|
+
import {
|
|
14
|
+
GAEventsNames,
|
|
15
|
+
QueuedEvent,
|
|
16
|
+
ConstantUrlParams,
|
|
17
|
+
SessionInfo,
|
|
18
|
+
EventUrlParams,
|
|
19
|
+
} from './interface';
|
|
8
20
|
|
|
9
21
|
const SEND_DEBOUNCE_MS = 5000;
|
|
10
22
|
const MAX_QUEUE_LENGTH = 10;
|
|
23
|
+
// eslint-disable-next-line no-magic-numbers
|
|
24
|
+
const SESSION_TIMEOUT = 30 * MINUTE_IN_MS;
|
|
11
25
|
|
|
12
26
|
const GA_CLIENT_ID = 'GA_client_id';
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
// You can use the following URL for debugging if the events are not getting collected.
|
|
16
|
-
// `https://www.google-analytics.com/debug/mp/collect?measurement_id=${id}&api_secret=${secret}`
|
|
17
|
-
`https://www.google-analytics.com/mp/collect?measurement_id=${id}&api_secret=${secret}`
|
|
18
|
-
);
|
|
27
|
+
const GA_SESSION_COUNTER = 'GA_session_counter';
|
|
28
|
+
const GA_FIRST_VISIT = 'GA_first_visit';
|
|
19
29
|
|
|
20
30
|
export class GoogleAnalytics implements AnalyticsClient {
|
|
21
31
|
name = 'GoogleAnalytics';
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
constantUrlParams: ConstantUrlParams = {
|
|
34
|
+
cid: '',
|
|
35
|
+
sid: 0,
|
|
36
|
+
uamb: 0,
|
|
37
|
+
uap: '',
|
|
38
|
+
uapv: '',
|
|
39
|
+
uam: '',
|
|
40
|
+
seg: 0,
|
|
41
|
+
sct: 0,
|
|
42
|
+
_p: generateUuid(),
|
|
43
|
+
};
|
|
26
44
|
|
|
27
45
|
a24iOperational = false;
|
|
28
46
|
|
|
29
|
-
|
|
47
|
+
lastOnEvent: number | undefined = undefined;
|
|
48
|
+
|
|
49
|
+
eventsQueue: QueuedEvent[] = [];
|
|
50
|
+
|
|
51
|
+
sessionTimeout = createTimeout();
|
|
52
|
+
|
|
53
|
+
isSessionActive = false;
|
|
30
54
|
|
|
31
|
-
|
|
55
|
+
sessionStartSent = false;
|
|
56
|
+
|
|
57
|
+
firstEvent = true;
|
|
58
|
+
|
|
59
|
+
firstVisit: boolean | undefined = undefined;
|
|
60
|
+
|
|
61
|
+
constructor(private id: string) {
|
|
32
62
|
this.init();
|
|
33
63
|
}
|
|
34
64
|
|
|
35
65
|
async init() {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
const params = this.constantUrlParams;
|
|
67
|
+
params.cid = await Storage.getItem(GA_CLIENT_ID) ?? '';
|
|
68
|
+
if (!params.cid) {
|
|
69
|
+
params.cid = generateUuid();
|
|
70
|
+
await Storage.setItem(GA_CLIENT_ID, params.cid);
|
|
40
71
|
}
|
|
72
|
+
this.firstVisit = !(await Storage.getItem(GA_FIRST_VISIT)) ?? true;
|
|
73
|
+
if (this.firstVisit) {
|
|
74
|
+
await Storage.setItem(GA_FIRST_VISIT, '0');
|
|
75
|
+
}
|
|
76
|
+
await this.startSession();
|
|
77
|
+
params.uap = await device.getPlatformName();
|
|
78
|
+
params.uapv = await device.getPlatformVersion();
|
|
79
|
+
params.uam = await device.getModelName();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async startSession() {
|
|
83
|
+
const sessionNumber = parseInt(await Storage.getItem(GA_SESSION_COUNTER) ?? '0') + 1;
|
|
84
|
+
this.constantUrlParams.sct = sessionNumber;
|
|
85
|
+
this.constantUrlParams.sid = generateSessionId();
|
|
86
|
+
await Storage.setItem(GA_SESSION_COUNTER, sessionNumber.toString());
|
|
87
|
+
this.isSessionActive = true;
|
|
88
|
+
this.setSessionTimeout();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setSessionTimeout() {
|
|
92
|
+
this.sessionTimeout.set(() => {
|
|
93
|
+
this.isSessionActive = false;
|
|
94
|
+
this.sessionStartSent = false;
|
|
95
|
+
this.constantUrlParams.seg = 0;
|
|
96
|
+
}, SESSION_TIMEOUT);
|
|
41
97
|
}
|
|
42
98
|
|
|
43
|
-
|
|
99
|
+
// @ts-ignore - cross event typing
|
|
100
|
+
async onEvent<Event extends keyof AnalyticsEventsMap>(
|
|
44
101
|
triggerName: Event,
|
|
45
|
-
payload: Parameters<AnalyticsEventsMap[Event]
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
102
|
+
payload: Parameters<AnalyticsEventsMap[Event]>[0],
|
|
103
|
+
) {
|
|
104
|
+
if (this.isSessionActive) {
|
|
105
|
+
this.setSessionTimeout();
|
|
106
|
+
} else {
|
|
107
|
+
await this.startSession();
|
|
108
|
+
}
|
|
109
|
+
const events = TRIGGER_NAME_TO_GA_EVENTS[
|
|
110
|
+
triggerName as typeof GOOGLE_ANALYTICS_TRIGGERS[number]
|
|
111
|
+
];
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const engagementTime = this.lastOnEvent ? now - this.lastOnEvent : undefined;
|
|
114
|
+
this.lastOnEvent = now;
|
|
115
|
+
await Promise.all(events.map(async (event, index) => {
|
|
49
116
|
if (event === 'a24i_operational' && !this.shouldContinueWithA24iOperational()) return;
|
|
50
|
-
const gtagPayload = mapPayload(
|
|
51
|
-
|
|
52
|
-
|
|
117
|
+
const gtagPayload = mapPayload(
|
|
118
|
+
event, triggerName, await this.getSessionInfo(
|
|
119
|
+
index === 0 ? engagementTime : undefined,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
const urlParams = mapEventParamsToUrlParams(payload as SceneInfo);
|
|
123
|
+
this.send(event, gtagPayload, urlParams);
|
|
124
|
+
}));
|
|
53
125
|
}
|
|
54
126
|
|
|
55
|
-
private send(name: GAEventsNames, params: Record<string, any
|
|
56
|
-
|
|
127
|
+
private send(name: GAEventsNames, params: Record<string, any>, urlParams: EventUrlParams) {
|
|
128
|
+
const lastUrlParams = JSON.stringify(
|
|
129
|
+
this.eventsQueue[this.eventsQueue.length - 1]?.urlParams ?? {},
|
|
130
|
+
);
|
|
131
|
+
if (lastUrlParams !== '{}' && lastUrlParams !== JSON.stringify(urlParams)) {
|
|
132
|
+
this.sendQueue();
|
|
133
|
+
}
|
|
134
|
+
if (this.constantUrlParams.seg === 0) {
|
|
135
|
+
this.eventsQueue.push({ name, params, urlParams });
|
|
136
|
+
this.sendQueue();
|
|
137
|
+
this.constantUrlParams.seg = 1;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.eventsQueue.push({ name, params, urlParams });
|
|
57
141
|
if (this.eventsQueue.length >= MAX_QUEUE_LENGTH) {
|
|
58
142
|
this.sendQueue();
|
|
59
143
|
} else {
|
|
@@ -62,13 +146,15 @@ export class GoogleAnalytics implements AnalyticsClient {
|
|
|
62
146
|
}
|
|
63
147
|
|
|
64
148
|
private sendQueue = () => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
149
|
+
if (this.eventsQueue.length === 0) return;
|
|
150
|
+
const body = prepareBody(this.eventsQueue);
|
|
151
|
+
const { urlParams } = this.eventsQueue[0];
|
|
69
152
|
this.eventsQueue.length = 0;
|
|
70
153
|
try {
|
|
71
|
-
fetch(
|
|
154
|
+
fetch(getUrl(this.id, {
|
|
155
|
+
...this.constantUrlParams,
|
|
156
|
+
...urlParams,
|
|
157
|
+
}), { method: 'POST', body });
|
|
72
158
|
} catch (e) {
|
|
73
159
|
// Effort to send was made.
|
|
74
160
|
}
|
|
@@ -81,4 +167,19 @@ export class GoogleAnalytics implements AnalyticsClient {
|
|
|
81
167
|
if (!a24iOperational) this.a24iOperational = true;
|
|
82
168
|
return !a24iOperational;
|
|
83
169
|
}
|
|
170
|
+
|
|
171
|
+
private async getSessionInfo(engagementTime: number | undefined): Promise<SessionInfo> {
|
|
172
|
+
const sessionStarted = !this.sessionStartSent;
|
|
173
|
+
this.sessionStartSent = true;
|
|
174
|
+
const { firstEvent } = this;
|
|
175
|
+
this.firstEvent = false;
|
|
176
|
+
const { firstVisit } = this;
|
|
177
|
+
this.firstVisit = false;
|
|
178
|
+
return {
|
|
179
|
+
sessionStarted,
|
|
180
|
+
engagementTime,
|
|
181
|
+
firstEvent,
|
|
182
|
+
firstVisit: firstVisit as boolean,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
84
185
|
}
|
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
import { AnalyticsEventsNames } from '../../interface';
|
|
2
1
|
import { GAEventsNames } from './interface';
|
|
3
2
|
|
|
4
|
-
export const
|
|
3
|
+
export const GOOGLE_ANALYTICS_TRIGGERS = [
|
|
4
|
+
'app_close',
|
|
5
|
+
'playback_10',
|
|
6
|
+
'playback_25',
|
|
7
|
+
'playback_50',
|
|
8
|
+
'playback_75',
|
|
9
|
+
'playback_90',
|
|
10
|
+
'playback_pause',
|
|
11
|
+
'playback_start',
|
|
12
|
+
'playback_stop',
|
|
13
|
+
'player_close',
|
|
14
|
+
'player_open',
|
|
15
|
+
'screen_view',
|
|
16
|
+
'scroll_25',
|
|
17
|
+
'scroll_50',
|
|
18
|
+
'scroll_75',
|
|
19
|
+
'scroll_90',
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export const TRIGGER_NAME_TO_GA_EVENTS: Record<
|
|
23
|
+
typeof GOOGLE_ANALYTICS_TRIGGERS[number], GAEventsNames[]
|
|
24
|
+
> = {
|
|
5
25
|
// App
|
|
6
26
|
'app_close': ['app_close', 'video_progress'],
|
|
7
27
|
|
|
8
|
-
// Data loading / Player buffering
|
|
9
|
-
'buffering_start': ['buffering_start'],
|
|
10
|
-
'buffering_stop': ['buffering_start'],
|
|
11
|
-
|
|
12
|
-
// Heartbeat
|
|
13
|
-
'heartbeat_5': ['a24i_content'],
|
|
14
|
-
|
|
15
28
|
// Playback
|
|
16
29
|
'playback_10': ['video_progress'],
|
|
17
30
|
'playback_25': ['video_progress'],
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ConstantUrlParams, EventUrlParams } from './interface';
|
|
2
|
+
|
|
3
|
+
const TRANSPORT_URL = 'https://www.google-analytics.com/g/collect?';
|
|
4
|
+
// You can use the following URL for debugging if the events are not getting collected.
|
|
5
|
+
// const TRANSPORT_URL = 'https://www.google-analytics.com/debug/g/collec?';
|
|
6
|
+
|
|
7
|
+
export const getUrl = (id: string, params: ConstantUrlParams & EventUrlParams) => {
|
|
8
|
+
const urlParts = [
|
|
9
|
+
TRANSPORT_URL,
|
|
10
|
+
`v=2&tid=${id}`,
|
|
11
|
+
];
|
|
12
|
+
(Object.keys(params) as (keyof (ConstantUrlParams & EventUrlParams))[])
|
|
13
|
+
.forEach((paramName: keyof (ConstantUrlParams & EventUrlParams)) => {
|
|
14
|
+
urlParts.push(`&${paramName}=${encodeURIComponent(params[paramName])}`);
|
|
15
|
+
});
|
|
16
|
+
return urlParts.join('');
|
|
17
|
+
};
|
|
@@ -8,3 +8,50 @@ export type GAEventsNames = (
|
|
|
8
8
|
| 'video_progress'
|
|
9
9
|
| 'video_start'
|
|
10
10
|
);
|
|
11
|
+
|
|
12
|
+
export type QueuedEvent = {
|
|
13
|
+
name: string,
|
|
14
|
+
params: Record<string, any>,
|
|
15
|
+
urlParams: EventUrlParams,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type SessionInfo = {
|
|
19
|
+
engagementTime: number | undefined,
|
|
20
|
+
sessionStarted: boolean,
|
|
21
|
+
firstEvent: boolean,
|
|
22
|
+
firstVisit: boolean,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ConstantUrlParams = {
|
|
26
|
+
/** Client ID (UUIv4 string) */
|
|
27
|
+
cid: string,
|
|
28
|
+
/** Session ID (random number) */
|
|
29
|
+
sid: number,
|
|
30
|
+
/** 1 for mobile devices, 0 otherwise */
|
|
31
|
+
uamb: number,
|
|
32
|
+
/** Platform name */
|
|
33
|
+
uap: string,
|
|
34
|
+
/** Platform version */
|
|
35
|
+
uapv: string,
|
|
36
|
+
/** Device model name */
|
|
37
|
+
uam: string,
|
|
38
|
+
/** Session engagement 0 for first event of session, 1 otherwise */
|
|
39
|
+
seg: number,
|
|
40
|
+
/** Session count */
|
|
41
|
+
sct: number,
|
|
42
|
+
/** Random page hash */
|
|
43
|
+
_p: string,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type EventUrlParams = {
|
|
47
|
+
/** Language iso639-1 */
|
|
48
|
+
ul: string,
|
|
49
|
+
/** Device resolution as WIDTHxHEIGHT */
|
|
50
|
+
sr: string,
|
|
51
|
+
/** Page hash route */
|
|
52
|
+
dl: string,
|
|
53
|
+
/** Page title */
|
|
54
|
+
dt: string,
|
|
55
|
+
/** Page referrer hash route */
|
|
56
|
+
dr: string,
|
|
57
|
+
};
|
|
@@ -1,76 +1,29 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const camelToSnake = (name: string) => (
|
|
13
|
-
name.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`)
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
const addKeyIfExists = <T extends string>(
|
|
17
|
-
keyName: T,
|
|
18
|
-
payload: { [key in typeof keyName]?: any },
|
|
19
|
-
gtagPayload: Record<string, any>,
|
|
20
|
-
) => {
|
|
21
|
-
if (payload[keyName]) gtagPayload[camelToSnake(keyName)] = payload[keyName];
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const mapGeneralData = (payload: Partial<GeneralAppData>) => {
|
|
25
|
-
const gtagPayloadGeneralData: Partial<GeneralAppData> = {};
|
|
26
|
-
addKeyIfExists('language', payload, gtagPayloadGeneralData);
|
|
27
|
-
addKeyIfExists('resolution', payload, gtagPayloadGeneralData);
|
|
28
|
-
return gtagPayloadGeneralData;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const mapSceneInfo = (payload: Partial<SceneInfo>) => {
|
|
32
|
-
const gtagPayloadSceneInfo: Partial<SceneInfo> = {};
|
|
33
|
-
addKeyIfExists('sceneId', payload, gtagPayloadSceneInfo);
|
|
34
|
-
addKeyIfExists('sceneType', payload, gtagPayloadSceneInfo);
|
|
35
|
-
addKeyIfExists('sceneName', payload, gtagPayloadSceneInfo);
|
|
36
|
-
return gtagPayloadSceneInfo;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const mapAppData = (payload: Partial<AppInfo>) => {
|
|
40
|
-
const gtagPayloadAppInfo: Partial<AppInfo> = {};
|
|
41
|
-
addKeyIfExists('applicationId', payload, gtagPayloadAppInfo);
|
|
42
|
-
addKeyIfExists('serviceId', payload, gtagPayloadAppInfo);
|
|
43
|
-
return gtagPayloadAppInfo;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const mapUserData = (payload: Partial<UserInfo>) => {
|
|
47
|
-
const gtagPayloadUserInfo: Partial<UserInfo> = {};
|
|
48
|
-
addKeyIfExists('userId', payload, gtagPayloadUserInfo);
|
|
49
|
-
return gtagPayloadUserInfo;
|
|
1
|
+
import type { AnalyticsEventsNames } from '../../interface';
|
|
2
|
+
import type { EventUrlParams, GAEventsNames, SessionInfo } from './interface';
|
|
3
|
+
|
|
4
|
+
export const mapEventParamsToUrlParams = (eventParams: Record<string, any>) => {
|
|
5
|
+
const urlParams: Partial<EventUrlParams> = {};
|
|
6
|
+
if (eventParams.language) urlParams.ul = eventParams.language;
|
|
7
|
+
if (eventParams.resolution) urlParams.sr = eventParams.resolution;
|
|
8
|
+
if (eventParams.sceneId) urlParams.dl = eventParams.sceneId;
|
|
9
|
+
if (eventParams.sceneName) urlParams.dt = eventParams.sceneName;
|
|
10
|
+
return urlParams as EventUrlParams;
|
|
50
11
|
};
|
|
51
12
|
|
|
52
13
|
export const mapPayload = (
|
|
53
14
|
eventName: GAEventsNames,
|
|
54
15
|
triggerName: AnalyticsEventsNames,
|
|
55
|
-
|
|
56
|
-
payload: Record<string, any>,
|
|
16
|
+
sessionInfo: SessionInfo,
|
|
57
17
|
) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
...mapUserData(payload),
|
|
18
|
+
const gtagPayload: Record<string, any> = {
|
|
19
|
+
'ep.event_trigger': triggerName,
|
|
20
|
+
_et: sessionInfo.engagementTime ?? 0,
|
|
21
|
+
_ee: sessionInfo.firstEvent ? 1 : 0,
|
|
22
|
+
_fv: sessionInfo.firstVisit ? 1 : 0,
|
|
23
|
+
_ss: sessionInfo.sessionStarted ? 1 : 0,
|
|
65
24
|
};
|
|
66
|
-
if (eventName === 'page_view' || eventName === 'buffering_start') {
|
|
67
|
-
gtagPayload = {
|
|
68
|
-
...gtagPayload,
|
|
69
|
-
...mapSceneInfo(payload),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
25
|
if (eventName === 'scroll') {
|
|
73
|
-
gtagPayload.percent_scroll = parseInt(triggerName.split('_')[1], 10);
|
|
26
|
+
gtagPayload['epn.percent_scroll'] = parseInt(triggerName.split('_')[1], 10);
|
|
74
27
|
}
|
|
75
28
|
return gtagPayload;
|
|
76
29
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { QueuedEvent } from './interface';
|
|
2
|
+
|
|
3
|
+
export const prepareBody = (events: QueuedEvent[]) => {
|
|
4
|
+
const eventLines = events.map((event: QueuedEvent) => {
|
|
5
|
+
const eventParams = [
|
|
6
|
+
`en=${event.name}`,
|
|
7
|
+
];
|
|
8
|
+
Object.keys(event.params).forEach((paramName) => {
|
|
9
|
+
eventParams.push(`${paramName}=${encodeURIComponent(event.params[paramName])}`);
|
|
10
|
+
});
|
|
11
|
+
return eventParams.join('&');
|
|
12
|
+
});
|
|
13
|
+
return eventLines.join('\r\n');
|
|
14
|
+
};
|
|
@@ -61,6 +61,8 @@ export class FirstOnlyGrid<T extends Item<U>, U>
|
|
|
61
61
|
const linesDiff = Math.floor(grid.index / itemsInGroup) - oldLine;
|
|
62
62
|
if (linesDiff > 0) {
|
|
63
63
|
this.scrollForward(linesDiff);
|
|
64
|
+
} else if (linesDiff < 0) {
|
|
65
|
+
this.scrollBackward(Math.abs(linesDiff));
|
|
64
66
|
}
|
|
65
67
|
};
|
|
66
68
|
|
|
@@ -77,8 +79,8 @@ export class FirstOnlyGrid<T extends Item<U>, U>
|
|
|
77
79
|
this.controller.scroll(targetScrollIndex);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
scrollBackward() {
|
|
81
|
-
const targetScrollIndex = this.controller.scrollIndex -
|
|
82
|
+
scrollBackward(diff = 1) {
|
|
83
|
+
const targetScrollIndex = this.controller.scrollIndex - diff;
|
|
82
84
|
this.controller.scroll(targetScrollIndex);
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
.keyboard-backdrop {
|
|
2
2
|
position: fixed;
|
|
3
3
|
left: 0;
|
|
4
|
+
right: 0;
|
|
4
5
|
top: 0;
|
|
5
6
|
width: $backdrop-width;
|
|
6
7
|
height: $backdrop-height;
|
|
@@ -9,6 +10,7 @@
|
|
|
9
10
|
.keyboard-backdrop-disabled {
|
|
10
11
|
position: absolute;
|
|
11
12
|
left: 0;
|
|
13
|
+
right: 0;
|
|
12
14
|
top: 0;
|
|
13
15
|
width: 100%;
|
|
14
16
|
height: 100%;
|
|
@@ -63,7 +63,12 @@ export class Keyboard extends KeyboardBase<Layouts, Props> {
|
|
|
63
63
|
show() {
|
|
64
64
|
super.show();
|
|
65
65
|
this.renderedLayout = 'normal';
|
|
66
|
-
|
|
66
|
+
const { layouts, languageLayout } = this.props;
|
|
67
|
+
if (this.isLanguageLayoutActive) {
|
|
68
|
+
this.renderLayout(this.languageFocusMatrix, languageLayout!.layout);
|
|
69
|
+
} else {
|
|
70
|
+
this.renderLayout(this.focusMatrices.normal, layouts.normal);
|
|
71
|
+
}
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
hide() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Component, createRef, DeclareProps } from '@24i/bigscreen-sdk/jsx';
|
|
2
2
|
import { PlayerBase } from '@24i/player-base';
|
|
3
|
-
import { switchByKey } from '@24i/bigscreen-sdk/device/keymap';
|
|
3
|
+
import { FF, REW, switchByKey } from '@24i/bigscreen-sdk/device/keymap';
|
|
4
4
|
import { stopEvent } from '@24i/bigscreen-sdk/utils/stopEvent';
|
|
5
5
|
import {
|
|
6
6
|
IDisplayToggler, getDisplayToggler, DISPLAY_NONE,
|
|
@@ -22,6 +22,7 @@ type Props = {
|
|
|
22
22
|
hidden?: boolean,
|
|
23
23
|
onClose?: () => void,
|
|
24
24
|
onHide?: () => void,
|
|
25
|
+
isPauseAllowed?: () => boolean,
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
export class PlayerUI extends Component<Props>
|
|
@@ -47,6 +48,7 @@ export class PlayerUI extends Component<Props>
|
|
|
47
48
|
hidden: false,
|
|
48
49
|
onClose: noop,
|
|
49
50
|
onHide: noop,
|
|
51
|
+
isPauseAllowed: () => true,
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
declare props: DeclareProps<Props, typeof PlayerUI.defaultProps>;
|
|
@@ -83,7 +85,7 @@ export class PlayerUI extends Component<Props>
|
|
|
83
85
|
FF: this.onKeyForward,
|
|
84
86
|
REW: this.onKeyRewind,
|
|
85
87
|
});
|
|
86
|
-
if (processed) stopEvent(event);
|
|
88
|
+
if (processed && !FF.is(event) && !REW.is(event)) stopEvent(event);
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
onKeyPlay = () => {
|
|
@@ -101,6 +103,7 @@ export class PlayerUI extends Component<Props>
|
|
|
101
103
|
};
|
|
102
104
|
|
|
103
105
|
onKeyPause = () => {
|
|
106
|
+
if (!this.props.isPauseAllowed()) return false;
|
|
104
107
|
if (this.seeking.isAutomaticallySeeking()) {
|
|
105
108
|
this.seeking.stopAutomaticSeek();
|
|
106
109
|
if (this.isPlaying()) this.trigger(PlayerAction.PAUSE);
|
|
@@ -115,6 +118,7 @@ export class PlayerUI extends Component<Props>
|
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
onKeyPlayPause = () => {
|
|
121
|
+
if (!this.props.isPauseAllowed()) return false;
|
|
118
122
|
if (this.seeking.isSeeking()) {
|
|
119
123
|
this.seeking.confirmSeek();
|
|
120
124
|
if (!this.isPlaying()) this.trigger(PlayerAction.PLAY);
|
|
@@ -38,6 +38,8 @@ export class Seekbar extends Component<Props> implements IFocusable {
|
|
|
38
38
|
|
|
39
39
|
duration = 0;
|
|
40
40
|
|
|
41
|
+
isMouseDownPressed = false;
|
|
42
|
+
|
|
41
43
|
static defaultProps = {
|
|
42
44
|
className: '',
|
|
43
45
|
};
|
|
@@ -46,18 +48,58 @@ export class Seekbar extends Component<Props> implements IFocusable {
|
|
|
46
48
|
const { ui } = this.props;
|
|
47
49
|
const playerInstance = ui.getPlayer();
|
|
48
50
|
this.duration = playerInstance.duration;
|
|
51
|
+
document.addEventListener('mouseup', this.onMouseUp);
|
|
52
|
+
document.addEventListener('mousemove', this.onMouseMove);
|
|
49
53
|
ui.seeking.addEventListener('seektimeupdate', this.onSeekTimeUpdate);
|
|
54
|
+
ui.seeking.addEventListener('stopautomaticseek', this.onStopAutomaticSeek);
|
|
50
55
|
ui.getPlayer().addEventListener('timeupdate', this.onTimeUpdate);
|
|
51
56
|
ui.getPlayer().addEventListener('durationchange', this.onDurationUpdate);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
componentWillUnmount() {
|
|
55
60
|
const { ui } = this.props;
|
|
61
|
+
document.removeEventListener('mouseup', this.onMouseUp);
|
|
62
|
+
document.removeEventListener('mousemove', this.onMouseMove);
|
|
56
63
|
ui.seeking.removeEventListener('seektimeupdate', this.onSeekTimeUpdate);
|
|
64
|
+
ui.seeking.removeEventListener('stopautomaticseek', this.onStopAutomaticSeek);
|
|
57
65
|
ui.getPlayer().removeEventListener('timeupdate', this.onTimeUpdate);
|
|
58
66
|
ui.getPlayer().removeEventListener('durationchange', this.onDurationUpdate);
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
onMouseMove = (event: MouseEvent) => {
|
|
70
|
+
if (!this.isMouseDownPressed) return;
|
|
71
|
+
const { ui: { seeking } } = this.props;
|
|
72
|
+
const wrapRect = this.wrap.current!.getBoundingClientRect();
|
|
73
|
+
const clientX = event.clientX - wrapRect.left;
|
|
74
|
+
const wrapRight = wrapRect.right - wrapRect.left;
|
|
75
|
+
let percentage = (TO_PERCENT * clientX) / wrapRight;
|
|
76
|
+
percentage = Math.max(0, Math.min(TO_PERCENT, percentage));
|
|
77
|
+
const seekTo = Math.round((this.duration / TO_PERCENT) * percentage);
|
|
78
|
+
seeking.setSeekTime(seekTo);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
onMouseClick = (event: MouseEvent) => {
|
|
82
|
+
const { ui: { seeking } } = this.props;
|
|
83
|
+
const wrapWidth = this.wrap.current!.offsetWidth;
|
|
84
|
+
const percentage = (TO_PERCENT * event.offsetX) / wrapWidth;
|
|
85
|
+
const seekTo = Math.round((this.duration / TO_PERCENT) * percentage);
|
|
86
|
+
seeking.setSeekTime(seekTo);
|
|
87
|
+
seeking.confirmSeek();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
onMouseDown = () => {
|
|
91
|
+
const { ui } = this.props;
|
|
92
|
+
this.isMouseDownPressed = true;
|
|
93
|
+
ui.disableHiding();
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
onMouseUp = () => {
|
|
97
|
+
const { ui } = this.props;
|
|
98
|
+
this.isMouseDownPressed = false;
|
|
99
|
+
ui.seeking.confirmSeek();
|
|
100
|
+
ui.enableHiding();
|
|
101
|
+
};
|
|
102
|
+
|
|
61
103
|
onKeyDown = (event: KeyboardEvent) => {
|
|
62
104
|
const processed = switchByKey(event, {
|
|
63
105
|
LEFT: this.onLeft,
|
|
@@ -123,6 +165,8 @@ export class Seekbar extends Component<Props> implements IFocusable {
|
|
|
123
165
|
this.updateSeekBar();
|
|
124
166
|
};
|
|
125
167
|
|
|
168
|
+
onStopAutomaticSeek = () => this.props.ui.enableHiding();
|
|
169
|
+
|
|
126
170
|
onTimeUpdate = ({ currentTime }: ITimeUpdateEvent) => {
|
|
127
171
|
const { ui } = this.props;
|
|
128
172
|
this.currentTime = currentTime;
|
|
@@ -183,6 +227,8 @@ export class Seekbar extends Component<Props> implements IFocusable {
|
|
|
183
227
|
ref={this.wrap}
|
|
184
228
|
tabIndex={0}
|
|
185
229
|
onKeyDown={this.onKeyDown}
|
|
230
|
+
onMouseDown={this.onMouseDown}
|
|
231
|
+
onClick={this.onMouseClick}
|
|
186
232
|
onBlur={this.onBlur}
|
|
187
233
|
>
|
|
188
234
|
<div className="bar">
|
|
@@ -95,6 +95,7 @@ export class Seeking implements IEvents<SeekingEventsMap> {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
stopAutomaticSeek() {
|
|
98
|
+
if (this.isAutomaticallySeeking()) this.triggerEvent('stopautomaticseek');
|
|
98
99
|
this.automaticSeekInterval.clear();
|
|
99
100
|
this.isAutomaticallySeekingForward = false;
|
|
100
101
|
this.isAutomaticallySeekingBackward = false;
|
|
@@ -22,6 +22,8 @@ export interface ActionsWithPayloadMap {
|
|
|
22
22
|
export interface IPlayerUI {
|
|
23
23
|
seeking: Seeking;
|
|
24
24
|
getPlayer: () => PlayerBase<any>,
|
|
25
|
+
disableHiding: () => void,
|
|
26
|
+
enableHiding: () => void,
|
|
25
27
|
heartbeat: () => void,
|
|
26
28
|
trigger<K extends keyof ActionsWithoutPayloadMap>(action: K): void,
|
|
27
29
|
trigger<K extends keyof ActionsWithPayloadMap>(
|
|
@@ -1,25 +1,59 @@
|
|
|
1
1
|
const ELLIPSIS = '...';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
const trimWithEllipsis = (
|
|
4
|
+
row: HTMLSpanElement | HTMLDivElement,
|
|
5
|
+
requestedNumberOfLines: number,
|
|
6
|
+
ellipsis = ELLIPSIS,
|
|
7
|
+
ending = false
|
|
8
|
+
) : void => {
|
|
9
|
+
if (!row.textContent) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
row.style.visibility = 'hidden';
|
|
13
|
+
const lineHeightString = window.getComputedStyle(row, null).getPropertyValue('line-height');
|
|
14
|
+
const lineHeight = parseInt(lineHeightString, 10);
|
|
15
|
+
let numberOfLines = row.offsetHeight / lineHeight;
|
|
16
|
+
if (numberOfLines > requestedNumberOfLines) {
|
|
17
|
+
const originalTextContent = row.textContent!;
|
|
18
|
+
row.textContent = ellipsis;
|
|
19
|
+
let stringBuffer = '';
|
|
20
|
+
if (ending) {
|
|
21
|
+
let currentPosition = 0;
|
|
22
|
+
do {
|
|
23
|
+
stringBuffer += originalTextContent.charAt(currentPosition);
|
|
24
|
+
row.textContent = stringBuffer + ellipsis;
|
|
25
|
+
currentPosition += 1;
|
|
26
|
+
numberOfLines = row.offsetHeight / lineHeight;
|
|
27
|
+
} while (numberOfLines <= requestedNumberOfLines
|
|
28
|
+
&& currentPosition < originalTextContent.length);
|
|
29
|
+
row.textContent = stringBuffer.substring(0, stringBuffer.length - 1) + ellipsis;
|
|
30
|
+
} else {
|
|
13
31
|
let currentPosition = originalTextContent.length - 1;
|
|
14
32
|
do {
|
|
15
33
|
const charToAdd = originalTextContent.at(currentPosition);
|
|
16
34
|
stringBuffer = `${charToAdd}${stringBuffer}`;
|
|
17
|
-
row.textContent = `${
|
|
35
|
+
row.textContent = `${ellipsis}${stringBuffer}`;
|
|
18
36
|
currentPosition -= 1;
|
|
19
37
|
numberOfLines = Math.floor(row.offsetHeight / lineHeight);
|
|
20
38
|
} while (numberOfLines <= requestedNumberOfLines && currentPosition >= 0);
|
|
21
|
-
row.textContent = `${
|
|
39
|
+
row.textContent = `${ellipsis}${stringBuffer.substring(1)}`;
|
|
22
40
|
}
|
|
23
|
-
row.style.visibility = '';
|
|
24
41
|
}
|
|
42
|
+
row.style.visibility = '';
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const trimWithLeadingEllipsis = (
|
|
46
|
+
row: HTMLSpanElement | HTMLDivElement,
|
|
47
|
+
requestedNumberOfLines: number,
|
|
48
|
+
ellipsis = ELLIPSIS,
|
|
49
|
+
) => {
|
|
50
|
+
return trimWithEllipsis(row, requestedNumberOfLines, ellipsis, false);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const trimWithEndingEllipsis = (
|
|
54
|
+
row: HTMLSpanElement | HTMLDivElement,
|
|
55
|
+
requestedNumberOfLines: number,
|
|
56
|
+
ellipsis = ELLIPSIS,
|
|
57
|
+
) => {
|
|
58
|
+
return trimWithEllipsis(row, requestedNumberOfLines, ellipsis, true);
|
|
25
59
|
};
|