@applicaster/zapp-react-native-utils 16.0.0-rc.121 → 16.0.0-rc.123
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/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
- package/appUtils/playerManager/__tests__/playerToolbox.test.ts +185 -1
- package/appUtils/playerManager/player.ts +15 -0
- package/appUtils/playerManager/playerToolbox.ts +41 -2
- package/manifestUtils/keys.js +1 -1
- package/package.json +3 -2
- package/quickBrickAssets/__tests__/index.test.ts +18 -0
- package/quickBrickAssets/index.ts +35 -0
- package/reactHooks/actions/index.ts +29 -23
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Player } from "../player";
|
|
2
|
+
|
|
3
|
+
const entry = (id: string) => ({ id, title: id }) as unknown as ZappEntry;
|
|
4
|
+
|
|
5
|
+
const makePlayer = () => {
|
|
6
|
+
const player = new Player({ playerId: "test-player" });
|
|
7
|
+
player.entry = entry("first");
|
|
8
|
+
|
|
9
|
+
return player;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("Player per-entry diagnostics", () => {
|
|
13
|
+
it("keeps the latest of onError and onVideoError, and clears it when a new entry loads", () => {
|
|
14
|
+
const player = makePlayer();
|
|
15
|
+
const second = { message: "second" };
|
|
16
|
+
|
|
17
|
+
player.onError({ message: "first" } as any);
|
|
18
|
+
player.onVideoError(second as any);
|
|
19
|
+
|
|
20
|
+
expect(player.lastError).toBe(second);
|
|
21
|
+
|
|
22
|
+
player.entry = entry("second");
|
|
23
|
+
|
|
24
|
+
expect(player.lastError).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("reads the next entry without creating the overlay observer", () => {
|
|
28
|
+
const player = makePlayer();
|
|
29
|
+
|
|
30
|
+
expect(player.getPlayNextEntry()).toBeNull();
|
|
31
|
+
|
|
32
|
+
jest
|
|
33
|
+
.spyOn(player.getOverlayObservable(), "getPlayNextEntry")
|
|
34
|
+
.mockReturnValue(entry("next"));
|
|
35
|
+
|
|
36
|
+
expect(player.getPlayNextEntry()).toEqual(entry("next"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("keeps the content clock while a client-side ad plays", () => {
|
|
40
|
+
const player = makePlayer();
|
|
41
|
+
|
|
42
|
+
player.onVideoProgress({
|
|
43
|
+
currentTime: 300,
|
|
44
|
+
duration: 1800,
|
|
45
|
+
seekableDuration: 1800,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
player.onAdBegin({});
|
|
49
|
+
|
|
50
|
+
player.onVideoProgress({
|
|
51
|
+
currentTime: 5,
|
|
52
|
+
duration: 15,
|
|
53
|
+
seekableDuration: 15,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(player.getPosition()).toBe(5);
|
|
57
|
+
expect(player.getDuration()).toBe(15);
|
|
58
|
+
expect(player.getContentPosition()).toBe(300);
|
|
59
|
+
expect(player.getContentDuration()).toBe(1800);
|
|
60
|
+
expect(player.getSeekableDuration()).toBe(1800);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -18,6 +18,9 @@ const loadToolbox = () => {
|
|
|
18
18
|
const player = (overrides = {}) => ({
|
|
19
19
|
getSeekableDuration: () => 100,
|
|
20
20
|
getContentPosition: () => 40,
|
|
21
|
+
isLive: () => false,
|
|
22
|
+
getPlayNextEntry: () => null,
|
|
23
|
+
playNextHandler: jest.fn(),
|
|
21
24
|
seekTo: jest.fn(),
|
|
22
25
|
...overrides,
|
|
23
26
|
});
|
|
@@ -30,7 +33,7 @@ const registeredTools = async (active) => {
|
|
|
30
33
|
mockXray = { registerReactToolbox: mockRegister };
|
|
31
34
|
await loadToolbox().registerPlayerToolbox(manager(active));
|
|
32
35
|
|
|
33
|
-
const { tools } = mockRegister.mock.
|
|
36
|
+
const { tools } = mockRegister.mock.lastCall[0];
|
|
34
37
|
|
|
35
38
|
return Object.fromEntries(tools.map((tool) => [tool.name, tool.handler]));
|
|
36
39
|
};
|
|
@@ -87,6 +90,187 @@ describe("playerToolbox", () => {
|
|
|
87
90
|
);
|
|
88
91
|
});
|
|
89
92
|
|
|
93
|
+
describe("state", () => {
|
|
94
|
+
const playing = (overrides = {}, entry: any = { id: "e1", title: "T" }) =>
|
|
95
|
+
player({
|
|
96
|
+
playerId: "p1",
|
|
97
|
+
playerRole: "Primary",
|
|
98
|
+
getEntry: () => entry,
|
|
99
|
+
getPosition: () => 40,
|
|
100
|
+
getDuration: () => 100,
|
|
101
|
+
getContentDuration: () => 100,
|
|
102
|
+
isReadyToPlay: () => true,
|
|
103
|
+
isPlaying: () => true,
|
|
104
|
+
isBuffering: () => false,
|
|
105
|
+
isAd: () => false,
|
|
106
|
+
isLive: () => false,
|
|
107
|
+
getIsMuted: () => false,
|
|
108
|
+
...overrides,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const state = async (overrides = {}, entry?: any) =>
|
|
112
|
+
(await registeredTools(playing(overrides, entry))).state();
|
|
113
|
+
|
|
114
|
+
it("reports inactive when nothing plays", async () => {
|
|
115
|
+
expect((await registeredTools(null)).state()).toEqual({
|
|
116
|
+
active: false,
|
|
117
|
+
casting: false,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("keeps the existing fields and adds the content clock for a VOD", async () => {
|
|
122
|
+
expect(await state()).toEqual({
|
|
123
|
+
active: true,
|
|
124
|
+
playerId: "p1",
|
|
125
|
+
entry: { id: "e1", title: "T" },
|
|
126
|
+
position: 40,
|
|
127
|
+
duration: 100,
|
|
128
|
+
playing: true,
|
|
129
|
+
buffering: false,
|
|
130
|
+
readyToPlay: true,
|
|
131
|
+
ad: false,
|
|
132
|
+
live: false,
|
|
133
|
+
muted: false,
|
|
134
|
+
role: "Primary",
|
|
135
|
+
plugin: null,
|
|
136
|
+
engine: null,
|
|
137
|
+
casting: false,
|
|
138
|
+
seekableDuration: 100,
|
|
139
|
+
contentPosition: 40,
|
|
140
|
+
contentDuration: 100,
|
|
141
|
+
drm: null,
|
|
142
|
+
lastError: null,
|
|
143
|
+
next: null,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("reports the content's clock next to the ad's during a client-side ad", async () => {
|
|
148
|
+
const result = await state({
|
|
149
|
+
isAd: () => true,
|
|
150
|
+
getPosition: () => 5,
|
|
151
|
+
getDuration: () => 15,
|
|
152
|
+
getContentPosition: () => 300,
|
|
153
|
+
getContentDuration: () => 1800,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(result).toMatchObject({
|
|
157
|
+
ad: true,
|
|
158
|
+
position: 5,
|
|
159
|
+
duration: 15,
|
|
160
|
+
contentPosition: 300,
|
|
161
|
+
contentDuration: 1800,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("reports non-finite numbers as null", async () => {
|
|
166
|
+
expect(
|
|
167
|
+
await state({
|
|
168
|
+
getSeekableDuration: () => NaN,
|
|
169
|
+
getContentPosition: () => null,
|
|
170
|
+
getContentDuration: () => Infinity,
|
|
171
|
+
})
|
|
172
|
+
).toMatchObject({
|
|
173
|
+
seekableDuration: null,
|
|
174
|
+
contentPosition: null,
|
|
175
|
+
contentDuration: null,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("reports the player plugin and its web engine", async () => {
|
|
180
|
+
expect(
|
|
181
|
+
await state({
|
|
182
|
+
playerPluginId: "QuickBrickPlayerPlugin",
|
|
183
|
+
engine: "hls.js",
|
|
184
|
+
})
|
|
185
|
+
).toMatchObject({ plugin: "QuickBrickPlayerPlugin", engine: "hls.js" });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("lists the entry's DRM key systems, or null", async () => {
|
|
189
|
+
const drm = { widevine: { license_url: "w" }, playready: {} };
|
|
190
|
+
|
|
191
|
+
expect((await state({}, { id: "e", extensions: { drm } })).drm).toEqual([
|
|
192
|
+
"widevine",
|
|
193
|
+
"playready",
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
expect((await state({}, { id: "e", extensions: { drm: {} } })).drm).toBe(
|
|
197
|
+
null
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect((await state({}, { id: "e", extensions: {} })).drm).toBe(null);
|
|
201
|
+
expect((await state({}, null)).drm).toBe(null);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("reports no last error as null", async () => {
|
|
205
|
+
expect((await state({ lastError: null })).lastError).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("play next", () => {
|
|
210
|
+
const nextEntry = { id: "n1", title: "Next", extensions: {} };
|
|
211
|
+
|
|
212
|
+
const stateWith = async (overrides) =>
|
|
213
|
+
(
|
|
214
|
+
await registeredTools(
|
|
215
|
+
player({
|
|
216
|
+
getEntry: () => null,
|
|
217
|
+
getPosition: () => 0,
|
|
218
|
+
getDuration: () => 0,
|
|
219
|
+
getContentDuration: () => 0,
|
|
220
|
+
isReadyToPlay: () => true,
|
|
221
|
+
isPlaying: () => true,
|
|
222
|
+
isBuffering: () => false,
|
|
223
|
+
isAd: () => false,
|
|
224
|
+
getIsMuted: () => false,
|
|
225
|
+
...overrides,
|
|
226
|
+
})
|
|
227
|
+
)
|
|
228
|
+
).state();
|
|
229
|
+
|
|
230
|
+
it("reports the entry Play Next would play", async () => {
|
|
231
|
+
expect(
|
|
232
|
+
(await stateWith({ getPlayNextEntry: () => nextEntry })).next
|
|
233
|
+
).toEqual({ id: "n1", title: "Next" });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("reports no next entry as null", async () => {
|
|
237
|
+
expect((await stateWith({})).next).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("plays the next entry through the player's Play Next action", async () => {
|
|
241
|
+
const active = player({ getPlayNextEntry: () => nextEntry });
|
|
242
|
+
const tools = await registeredTools(active);
|
|
243
|
+
|
|
244
|
+
expect(tools.next()).toEqual({ id: "n1", title: "Next" });
|
|
245
|
+
expect(active.playNextHandler).toHaveBeenCalledTimes(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("fails without a next entry", async () => {
|
|
249
|
+
const active = player();
|
|
250
|
+
const tools = await registeredTools(active);
|
|
251
|
+
|
|
252
|
+
expect(() => tools.next()).toThrow("No next item");
|
|
253
|
+
expect(active.playNextHandler).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("fails when no Play Next action is registered", async () => {
|
|
257
|
+
const tools = await registeredTools(
|
|
258
|
+
player({
|
|
259
|
+
getPlayNextEntry: () => nextEntry,
|
|
260
|
+
playNextHandler: null,
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(() => tools.next()).toThrow("No next item");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("fails without an active player", async () => {
|
|
268
|
+
const tools = await registeredTools(null);
|
|
269
|
+
|
|
270
|
+
expect(() => tools.next()).toThrow("No active player");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
90
274
|
it("refuses to seek a stream that is not seekable", async () => {
|
|
91
275
|
const tools = await registeredTools(
|
|
92
276
|
player({ getSeekableDuration: () => 0 })
|
|
@@ -76,6 +76,12 @@ export class Player {
|
|
|
76
76
|
private overlaysObserver?: OverlaysObserver | null;
|
|
77
77
|
private entryObservable?: BehaviorSubject<ZappEntry> | null;
|
|
78
78
|
private contentSubject?: BehaviorSubject<PlayerContent> | null;
|
|
79
|
+
// Read by the X-Ray `player` toolbox: the last onError/onVideoError of the current entry,
|
|
80
|
+
// and the Play Next action of the PlayerContainer showing this player.
|
|
81
|
+
public lastError: unknown = null;
|
|
82
|
+
public playNextHandler: (() => void) | null = null;
|
|
83
|
+
// The web engine the default player picked (hls.js, dash.js, shaka, video.js); null elsewhere.
|
|
84
|
+
public engine: string | null = null;
|
|
79
85
|
|
|
80
86
|
protected listeners: {
|
|
81
87
|
[key: string]: QuickBrickPlayer.SharedPlayerCallBacks;
|
|
@@ -89,6 +95,7 @@ export class Player {
|
|
|
89
95
|
|
|
90
96
|
set entry(newEntry: ZappEntry | null) {
|
|
91
97
|
this._entry = newEntry;
|
|
98
|
+
this.lastError = null;
|
|
92
99
|
// The live entry is scoped to the entry it overlays - see
|
|
93
100
|
// `OverlaysObserver.clearLiveEntry`. No-op when there is no observer,
|
|
94
101
|
// which is every non-live player.
|
|
@@ -271,6 +278,10 @@ export class Player {
|
|
|
271
278
|
|
|
272
279
|
getEntry = (): ZappEntry | null => this.entry;
|
|
273
280
|
|
|
281
|
+
/** The entry Play Next would play now; null without one. Never creates the overlay observer. */
|
|
282
|
+
getPlayNextEntry = (): ZappEntry | null =>
|
|
283
|
+
this.overlaysObserver?.getPlayNextEntry() ?? null;
|
|
284
|
+
|
|
274
285
|
logState = (text, additionalParams = {}) => {
|
|
275
286
|
const isSeeking = this.playerState.seekPosition !== null;
|
|
276
287
|
|
|
@@ -492,6 +503,8 @@ export class Player {
|
|
|
492
503
|
};
|
|
493
504
|
|
|
494
505
|
onVideoError = (error: Error) => {
|
|
506
|
+
this.lastError = error;
|
|
507
|
+
|
|
495
508
|
this.invokeListenersUpdate({
|
|
496
509
|
callbackName: SharedPlayerCallBacksKeys.OnVideoError,
|
|
497
510
|
event: error,
|
|
@@ -499,6 +512,8 @@ export class Player {
|
|
|
499
512
|
};
|
|
500
513
|
|
|
501
514
|
onError = (error: Error) => {
|
|
515
|
+
this.lastError = error;
|
|
516
|
+
|
|
502
517
|
this.invokeListenersUpdate({
|
|
503
518
|
callbackName: SharedPlayerCallBacksKeys.OnError,
|
|
504
519
|
event: error,
|
|
@@ -14,6 +14,12 @@ const toNumber = (value: unknown) =>
|
|
|
14
14
|
|
|
15
15
|
const clamp = (value: number, max: number) => Math.min(Math.max(value, 0), max);
|
|
16
16
|
|
|
17
|
+
/** Key systems the entry declares (widevine, playready, fairplay...). */
|
|
18
|
+
const drmKeySystems = (drm: unknown) =>
|
|
19
|
+
drm && typeof drm === "object" && Object.keys(drm).length
|
|
20
|
+
? Object.keys(drm)
|
|
21
|
+
: null;
|
|
22
|
+
|
|
17
23
|
/** The range a seek can reach: the DVR window on live, the content on VOD. */
|
|
18
24
|
const requireSeekable = (player: {
|
|
19
25
|
getSeekableDuration: () => number | undefined;
|
|
@@ -38,14 +44,21 @@ const tools = (manager: PlayerManager): XrayToolSpec[] => {
|
|
|
38
44
|
{
|
|
39
45
|
name: "state",
|
|
40
46
|
description:
|
|
41
|
-
"The active player: entry, position and duration in seconds
|
|
42
|
-
"role (Primary, Cell, Chromecast) and casting
|
|
47
|
+
"The active player: entry, position and duration in seconds (the ad's while an ad plays), " +
|
|
48
|
+
"playing/buffering/ad/live/muted, role (Primary, Cell, Chromecast) and casting; in seconds, " +
|
|
49
|
+
"seekableDuration and contentPosition/contentDuration (the content's, even during ads); " +
|
|
50
|
+
"drm key systems, lastError {message} and next {id, title}, each null when absent; " +
|
|
51
|
+
"plugin (player plugin id) and engine (the default player's web engine, else null). " +
|
|
52
|
+
"active: false when nothing plays.",
|
|
43
53
|
handler: () => {
|
|
44
54
|
const player = manager.getActivePlayer();
|
|
45
55
|
|
|
46
56
|
if (!player) return { active: false, casting: manager.isCasting() };
|
|
47
57
|
|
|
48
58
|
const entry = player.getEntry();
|
|
59
|
+
const seekableDuration = toNumber(player.getSeekableDuration());
|
|
60
|
+
const next = player.getPlayNextEntry();
|
|
61
|
+
const error: any = player.lastError;
|
|
49
62
|
|
|
50
63
|
return {
|
|
51
64
|
active: true,
|
|
@@ -61,7 +74,18 @@ const tools = (manager: PlayerManager): XrayToolSpec[] => {
|
|
|
61
74
|
live: !!player.isLive(),
|
|
62
75
|
muted: !!player.getIsMuted(),
|
|
63
76
|
role: player.playerRole,
|
|
77
|
+
plugin: player.playerPluginId ?? null,
|
|
78
|
+
engine: player.engine ?? null,
|
|
64
79
|
casting: manager.isCasting(),
|
|
80
|
+
seekableDuration,
|
|
81
|
+
contentPosition: toNumber(player.getContentPosition()),
|
|
82
|
+
contentDuration: toNumber(player.getContentDuration()),
|
|
83
|
+
drm: drmKeySystems(entry?.extensions?.drm),
|
|
84
|
+
lastError:
|
|
85
|
+
error == null
|
|
86
|
+
? null
|
|
87
|
+
: { message: String(error?.message ?? error?.error ?? error) },
|
|
88
|
+
next: next ? { id: next.id, title: next.title } : null,
|
|
65
89
|
};
|
|
66
90
|
},
|
|
67
91
|
},
|
|
@@ -83,6 +107,21 @@ const tools = (manager: PlayerManager): XrayToolSpec[] => {
|
|
|
83
107
|
return "OK";
|
|
84
108
|
},
|
|
85
109
|
},
|
|
110
|
+
{
|
|
111
|
+
name: "next",
|
|
112
|
+
description:
|
|
113
|
+
"Play the next item, as the Play Next overlay does. Fails when there is none.",
|
|
114
|
+
handler: () => {
|
|
115
|
+
const player = requirePlayer();
|
|
116
|
+
const next = player.getPlayNextEntry();
|
|
117
|
+
|
|
118
|
+
if (!next || !player.playNextHandler) throw new Error("No next item");
|
|
119
|
+
|
|
120
|
+
player.playNextHandler();
|
|
121
|
+
|
|
122
|
+
return { id: next.id, title: next.title };
|
|
123
|
+
},
|
|
124
|
+
},
|
|
86
125
|
{
|
|
87
126
|
name: "seek",
|
|
88
127
|
description:
|
package/manifestUtils/keys.js
CHANGED
|
@@ -484,7 +484,7 @@ const MOBILE_ACTION_BUTTON_FIELDS = [
|
|
|
484
484
|
{ text: "share", value: "native_share_action" },
|
|
485
485
|
{ text: "downloads", value: "offline-content-button" },
|
|
486
486
|
{ text: "trailer", value: "trailer_action" },
|
|
487
|
-
{ text: "mute/unmute", value: "
|
|
487
|
+
{ text: "mute/unmute", value: "mute_unmute_action" },
|
|
488
488
|
],
|
|
489
489
|
},
|
|
490
490
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "16.0.0-rc.
|
|
3
|
+
"version": "16.0.0-rc.123",
|
|
4
4
|
"description": "Applicaster Zapp React Native utilities package",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@applicaster/applicaster-types": "16.0.0-rc.
|
|
30
|
+
"@applicaster/applicaster-types": "16.0.0-rc.123",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"prop-types": "^15.0.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
+
"@applicaster/quick-brick-assets": "*",
|
|
39
40
|
"@applicaster/zapp-pipes-v2-client": "*",
|
|
40
41
|
"@react-native-community/netinfo": "*",
|
|
41
42
|
"react": "*",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The Jest stand-in for @applicaster/quick-brick-assets maps each existing key to itself
|
|
2
|
+
import { getQuickBrickAsset } from "..";
|
|
3
|
+
|
|
4
|
+
describe("getQuickBrickAsset", () => {
|
|
5
|
+
it("returns the asset for a plugin identifier and path", () => {
|
|
6
|
+
expect(
|
|
7
|
+
getQuickBrickAsset("mute_unmute_action", "action/flavour_1/mute.png")
|
|
8
|
+
).toBe("mute_unmute_action/action/flavour_1/mute.png");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("rejects unknown paths and plugin identifiers at compile time", () => {
|
|
12
|
+
// @ts-expect-error - no such file in mute_unmute_action
|
|
13
|
+
getQuickBrickAsset("mute_unmute_action", "action/flavour_9/mute.png");
|
|
14
|
+
|
|
15
|
+
// @ts-expect-error - no assets folder for this plugin
|
|
16
|
+
getQuickBrickAsset("unknown_plugin", "action/flavour_1/mute.png");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assets from "@applicaster/quick-brick-assets";
|
|
2
|
+
import type {
|
|
3
|
+
QuickBrickAssetKey,
|
|
4
|
+
QuickBrickAssetSource,
|
|
5
|
+
} from "@applicaster/quick-brick-assets";
|
|
6
|
+
|
|
7
|
+
export type { QuickBrickAssetKey, QuickBrickAssetSource };
|
|
8
|
+
|
|
9
|
+
type PluginOf<K> = K extends `${infer Plugin}/${string}` ? Plugin : never;
|
|
10
|
+
|
|
11
|
+
type PathOf<K, Plugin extends string> = K extends `${Plugin}/${infer Path}`
|
|
12
|
+
? Path
|
|
13
|
+
: never;
|
|
14
|
+
|
|
15
|
+
/** Plugin identifiers that have a folder in `@applicaster/quick-brick-assets`. */
|
|
16
|
+
export type QuickBrickAssetPlugin = PluginOf<QuickBrickAssetKey>;
|
|
17
|
+
|
|
18
|
+
/** Asset paths inside the given plugin's folder. */
|
|
19
|
+
export type QuickBrickAssetPath<Plugin extends QuickBrickAssetPlugin> = PathOf<
|
|
20
|
+
QuickBrickAssetKey,
|
|
21
|
+
Plugin
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Looks up an asset of `@applicaster/quick-brick-assets` by plugin identifier
|
|
26
|
+
* and path relative to the plugin folder, e.g.
|
|
27
|
+
* `getQuickBrickAsset("mute_unmute_action", "action/flavour_1/mute.png")`.
|
|
28
|
+
* Both arguments are checked against the package's files at compile time.
|
|
29
|
+
* Density variants (`@2x` / `@3x`) are not supported.
|
|
30
|
+
*/
|
|
31
|
+
export const getQuickBrickAsset = <Plugin extends QuickBrickAssetPlugin>(
|
|
32
|
+
pluginIdentifier: Plugin,
|
|
33
|
+
assetPath: QuickBrickAssetPath<Plugin>
|
|
34
|
+
): QuickBrickAssetSource =>
|
|
35
|
+
assets[`${pluginIdentifier}/${assetPath}` as QuickBrickAssetKey];
|
|
@@ -21,17 +21,45 @@ type ActionContexts<T = unknown> = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* A value that changes whenever a provider is added to or removed from
|
|
25
|
+
* `uiActionsRegistry`.
|
|
26
|
+
*
|
|
27
|
+
* Providers can register after the UI that wants them has rendered - the audio
|
|
28
|
+
* player registers playback speed only once its controller has state, and
|
|
29
|
+
* plugin-backed actions are bridged in from an effect. Put this in the
|
|
30
|
+
* dependency list of anything that resolves actions, so the list is re-read
|
|
31
|
+
* instead of being frozen at first paint.
|
|
32
|
+
*/
|
|
33
|
+
export function useRegistryRevision(): number {
|
|
34
|
+
const [revision, setRevision] = useState(0);
|
|
35
|
+
|
|
36
|
+
useEffect(
|
|
37
|
+
() =>
|
|
38
|
+
uiActionsRegistry.subscribe(() => setRevision((current) => current + 1)),
|
|
39
|
+
[]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return revision;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolves a legacy action context or falls back to the UI action registry.
|
|
25
47
|
* @param {*} plugId - subscribes to the context of that action plugin
|
|
26
48
|
* @return {*} - global action provider context or context for specific action plugin
|
|
27
49
|
*/
|
|
28
50
|
export function useActions<T = unknown>(plugId: string): any {
|
|
29
51
|
const [logged, setLogged] = useState(false);
|
|
30
52
|
const context = useContext<ActionContexts<T>>(ActionsContext);
|
|
53
|
+
useRegistryRevision();
|
|
54
|
+
|
|
31
55
|
const actionContext = context?.actions?.[plugId]?.module?.context;
|
|
32
56
|
|
|
33
57
|
if (plugId) {
|
|
34
58
|
if (!actionContext) {
|
|
59
|
+
const registeredAction = uiActionsRegistry.getAction(plugId);
|
|
60
|
+
|
|
61
|
+
if (registeredAction) return registeredAction;
|
|
62
|
+
|
|
35
63
|
if (!logged) {
|
|
36
64
|
logger.warning({
|
|
37
65
|
message: `useActions: Couldn't find an action for ${plugId} plugin`,
|
|
@@ -139,25 +167,3 @@ export function useEntryActionState(
|
|
|
139
167
|
|
|
140
168
|
return { state, invokeAction };
|
|
141
169
|
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* A value that changes whenever a provider is added to or removed from
|
|
145
|
-
* `uiActionsRegistry`.
|
|
146
|
-
*
|
|
147
|
-
* Providers can register after the UI that wants them has rendered - the audio
|
|
148
|
-
* player registers playback speed only once its controller has state, and
|
|
149
|
-
* plugin-backed actions are bridged in from an effect. Put this in the
|
|
150
|
-
* dependency list of anything that resolves actions, so the list is re-read
|
|
151
|
-
* instead of being frozen at first paint.
|
|
152
|
-
*/
|
|
153
|
-
export function useRegistryRevision(): number {
|
|
154
|
-
const [revision, setRevision] = useState(0);
|
|
155
|
-
|
|
156
|
-
useEffect(
|
|
157
|
-
() =>
|
|
158
|
-
uiActionsRegistry.subscribe(() => setRevision((current) => current + 1)),
|
|
159
|
-
[]
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
return revision;
|
|
163
|
-
}
|