@streamscloud/embeddable 2.1.5 → 2.2.0

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.
@@ -7,3 +7,4 @@ export { default as ShortVideoDetails } from './cmp.short-video-details.svelte';
7
7
  export type { ShortVideoViewerModel, ShortVideoViewerAdModel, ShortVideoViewerProductModel } from './types';
8
8
  export type { IShortVideoViewerLocalization } from './short-video-viewer-localization.svelte';
9
9
  export type { IShortVideoDetailsLocalization } from './short-video-details-localization.svelte';
10
+ export { mapShortVideoViewerModel } from './mapper';
@@ -4,3 +4,4 @@ export { default as ShortVideoProductViewer } from './cmp.product.svelte';
4
4
  export { default as ShortVideoViewerAttachments } from './cmp.attachments.svelte';
5
5
  export { default as ShortVideoViewerAttachmentsInline } from './cmp.attachments-inline.svelte';
6
6
  export { default as ShortVideoDetails } from './cmp.short-video-details.svelte';
7
+ export { mapShortVideoViewerModel } from './mapper';
@@ -7,16 +7,64 @@
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { ShortVideoViewer } from '../short-video-viewer';
10
+ import { createLocalGQLClient } from '../../core/graphql';
11
+ import { mapShortVideoViewerModel, ShortVideoViewer } from '../short-video-viewer';
12
+ import { GetShortVideosDocument } from './operations.generated';
13
+ import { Loading } from '../../ui/loading';
11
14
  import { PlayerBuffer, PlayerSlider } from '../../ui/player';
12
15
  import { SpotlightLayout } from '../../ui/spotlight-layout';
13
16
  import { default as Controls } from './controls.svelte';
14
17
  import { ShortVideosPlayerLocalization } from './short-videos-player-localization.svelte';
15
18
  import { ShortVideosPlayerUiManager } from './ui-manager.svelte';
16
- import { onDestroy, onMount } from 'svelte';
17
- let { shortVideosProvider, localization: localizationInit, on } = $props();
19
+ import { onDestroy, onMount, untrack } from 'svelte';
20
+ let { input, localization: localizationInit, on } = $props();
18
21
  const localization = $derived(new ShortVideosPlayerLocalization(localizationInit));
19
- const buffer = $derived(new PlayerBuffer(shortVideosProvider));
22
+ let buffer = $state(input.type === 'provider' ? new PlayerBuffer(input.provider) : null);
23
+ $effect(() => {
24
+ if (input.type !== 'ids')
25
+ return;
26
+ untrack(() => {
27
+ initBuffer(input);
28
+ });
29
+ });
30
+ const initBuffer = (input) => __awaiter(void 0, void 0, void 0, function* () {
31
+ var _a, _b;
32
+ try {
33
+ const { graphqlUrl, ids, initialId } = input;
34
+ const graphql = createLocalGQLClient(graphqlUrl);
35
+ const payload = yield graphql
36
+ .query(GetShortVideosDocument, {
37
+ input: {
38
+ filter: {
39
+ includePostIds: ids
40
+ }
41
+ }
42
+ })
43
+ .toPromise();
44
+ const posts = ((_b = (_a = payload.data) === null || _a === void 0 ? void 0 : _a.shortVideos) === null || _b === void 0 ? void 0 : _b.items) || [];
45
+ const idOrder = new Map(ids.map((id, index) => [id, index]));
46
+ posts.sort((a, b) => {
47
+ var _a, _b;
48
+ return ((_a = idOrder.get(a.id)) !== null && _a !== void 0 ? _a : Infinity) - ((_b = idOrder.get(b.id)) !== null && _b !== void 0 ? _b : Infinity);
49
+ });
50
+ const index = initialId ? posts.findIndex((p) => p.id === initialId) : 0;
51
+ const provider = {
52
+ initialData: {
53
+ prefetchedItems: posts.map(mapShortVideoViewerModel),
54
+ startIndex: index
55
+ },
56
+ loadMore: () => __awaiter(void 0, void 0, void 0, function* () {
57
+ // No more items to load, as all are already prefetched
58
+ return [];
59
+ })
60
+ };
61
+ buffer = new PlayerBuffer(provider);
62
+ }
63
+ catch (_c) {
64
+ console.error('Failed to load short videos by IDs:', input.ids);
65
+ buffer = null; // Reset buffer on error
66
+ }
67
+ });
20
68
  const uiManager = new ShortVideosPlayerUiManager();
21
69
  onMount(() => __awaiter(void 0, void 0, void 0, function* () {
22
70
  uiManager.detailsCollapsed = window && window.innerWidth < window.innerHeight;
@@ -45,6 +93,8 @@ const handleDimensionsChanged = (dimensions) => {
45
93
  </div>
46
94
  </SpotlightLayout>
47
95
  <Controls buffer={buffer} uiManager={uiManager} localization={localization} on={{ closePlayer: () => on?.closePlayer?.() }} />
96
+ {:else}
97
+ <Loading positionFixedCenter={true} timeout={1000} />
48
98
  {/if}
49
99
  </div>
50
100
  </div>
@@ -1,8 +1,7 @@
1
- import { type ShortVideoViewerModel } from '../short-video-viewer';
2
- import { type PlayerItemsProvider } from '../../ui/player';
1
+ import type { PlayerInput } from './types';
3
2
  import { type IShortVideosPlayerLocalization } from './short-videos-player-localization.svelte';
4
3
  type Props = {
5
- shortVideosProvider: PlayerItemsProvider<ShortVideoViewerModel>;
4
+ input: PlayerInput;
6
5
  localization?: IShortVideosPlayerLocalization;
7
6
  on?: {
8
7
  closePlayer?: () => void;
@@ -1,15 +1,38 @@
1
- import type { ShortVideoViewerModel } from '../short-video-viewer';
2
- import type { PlayerItemsProvider } from '../../ui/player';
1
+ import { type OpenShortVideosPlayerInit } from './types';
3
2
  import type { IShortVideosPlayerLocalization } from './short-videos-player-localization.svelte';
4
3
  export type { IShortVideosPlayerLocalization };
5
4
  /**
6
- * Opens the short videos player modal with the specified provider and optional localization.
5
+ * Opens the short videos player modal with the specified provider or by fetching videos by IDs.
6
+ *
7
+ * You can use either a ready-made provider, or simply pass a list of video IDs and a GraphQL endpoint to fetch them.
7
8
  *
8
9
  * @param init - Configuration options.
9
- * @param {PlayerItemsProvider<ShortVideoViewerModel>} init.shortVideosProvider - The provider instance supplying short video items to the player.
10
- * @param {IShortVideosPlayerLocalization} [init.localization] - Optional localization settings for the player UI.
11
10
  *
12
- * @example
11
+ * @param {PlayerItemsProvider<ShortVideoViewerModel>} [init.shortVideosProvider]
12
+ * The provider instance supplying short video items to the player.
13
+ * **Use this if you already have your own provider implementation.**
14
+ *
15
+ * @param {string[]} [init.ids]
16
+ * List of short video IDs to display in the player.
17
+ * **Use this if you want the player to fetch and show specific videos.**
18
+ *
19
+ * @param {string} [init.graphqlUrl]
20
+ * The GraphQL endpoint to use when fetching videos by IDs.
21
+ * **Required if you use `ids`.**
22
+ *
23
+ * @param {string} [init.initialId]
24
+ * The ID of the video that should be shown first.
25
+ * **Optional. Only used when using `ids`.**
26
+ *
27
+ * @param {IShortVideosPlayerLocalization} [init.localization]
28
+ * Optional localization settings for the player UI.
29
+ *
30
+ * @param {object} [init.on]
31
+ * Optional event handlers.
32
+ * @param {() => void} [init.on.playerClosed]
33
+ * Called when the player is closed.
34
+ *
35
+ * @example <caption>Using a custom provider</caption>
13
36
  * ```ts
14
37
  * import { openShortVideosPlayer } from '@streamscloud/embeddable/short-videos-player';
15
38
  *
@@ -21,11 +44,25 @@ export type { IShortVideosPlayerLocalization };
21
44
  * }
22
45
  * });
23
46
  * ```
47
+ *
48
+ * @example <caption>Using a list of IDs</caption>
49
+ * ```ts
50
+ * import { openShortVideosPlayer } from '@streamscloud/embeddable/short-videos-player';
51
+ *
52
+ * openShortVideosPlayer({
53
+ * ids: ['id1', 'id2', 'id3'],
54
+ * graphqlUrl: 'https://api.example.com/graphql',
55
+ * initialId: 'id2',
56
+ * localization: {
57
+ * next: 'Next',
58
+ * previous: 'Previous'
59
+ * },
60
+ * on: {
61
+ * playerClosed: () => {
62
+ * console.log('Player was closed');
63
+ * }
64
+ * }
65
+ * });
66
+ * ```
24
67
  */
25
- export declare const openShortVideosPlayer: (init: {
26
- shortVideosProvider: PlayerItemsProvider<ShortVideoViewerModel>;
27
- localization?: IShortVideosPlayerLocalization;
28
- on?: {
29
- playerClosed?: () => void;
30
- };
31
- }) => void;
68
+ export declare const openShortVideosPlayer: (init: OpenShortVideosPlayerInit) => Promise<void>;
@@ -1,14 +1,39 @@
1
+ import { isIdsInit, isShortVideosProviderInit } from './types';
1
2
  import { ShadowHost } from '../../ui/shadow-dom';
2
- import { default as ShortVideosPlayer } from './cmp.short-videos-player.svelte';
3
3
  import { mount } from 'svelte';
4
+ import { default as ShortVideosPlayer } from './cmp.short-videos-player.svelte';
4
5
  /**
5
- * Opens the short videos player modal with the specified provider and optional localization.
6
+ * Opens the short videos player modal with the specified provider or by fetching videos by IDs.
7
+ *
8
+ * You can use either a ready-made provider, or simply pass a list of video IDs and a GraphQL endpoint to fetch them.
6
9
  *
7
10
  * @param init - Configuration options.
8
- * @param {PlayerItemsProvider<ShortVideoViewerModel>} init.shortVideosProvider - The provider instance supplying short video items to the player.
9
- * @param {IShortVideosPlayerLocalization} [init.localization] - Optional localization settings for the player UI.
10
11
  *
11
- * @example
12
+ * @param {PlayerItemsProvider<ShortVideoViewerModel>} [init.shortVideosProvider]
13
+ * The provider instance supplying short video items to the player.
14
+ * **Use this if you already have your own provider implementation.**
15
+ *
16
+ * @param {string[]} [init.ids]
17
+ * List of short video IDs to display in the player.
18
+ * **Use this if you want the player to fetch and show specific videos.**
19
+ *
20
+ * @param {string} [init.graphqlUrl]
21
+ * The GraphQL endpoint to use when fetching videos by IDs.
22
+ * **Required if you use `ids`.**
23
+ *
24
+ * @param {string} [init.initialId]
25
+ * The ID of the video that should be shown first.
26
+ * **Optional. Only used when using `ids`.**
27
+ *
28
+ * @param {IShortVideosPlayerLocalization} [init.localization]
29
+ * Optional localization settings for the player UI.
30
+ *
31
+ * @param {object} [init.on]
32
+ * Optional event handlers.
33
+ * @param {() => void} [init.on.playerClosed]
34
+ * Called when the player is closed.
35
+ *
36
+ * @example <caption>Using a custom provider</caption>
12
37
  * ```ts
13
38
  * import { openShortVideosPlayer } from '@streamscloud/embeddable/short-videos-player';
14
39
  *
@@ -20,15 +45,55 @@ import { mount } from 'svelte';
20
45
  * }
21
46
  * });
22
47
  * ```
48
+ *
49
+ * @example <caption>Using a list of IDs</caption>
50
+ * ```ts
51
+ * import { openShortVideosPlayer } from '@streamscloud/embeddable/short-videos-player';
52
+ *
53
+ * openShortVideosPlayer({
54
+ * ids: ['id1', 'id2', 'id3'],
55
+ * graphqlUrl: 'https://api.example.com/graphql',
56
+ * initialId: 'id2',
57
+ * localization: {
58
+ * next: 'Next',
59
+ * previous: 'Previous'
60
+ * },
61
+ * on: {
62
+ * playerClosed: () => {
63
+ * console.log('Player was closed');
64
+ * }
65
+ * }
66
+ * });
67
+ * ```
23
68
  */
24
- export const openShortVideosPlayer = (init) => {
25
- const { shortVideosProvider, localization } = init;
69
+ export const openShortVideosPlayer = async (init) => {
70
+ let input = null;
71
+ if (isShortVideosProviderInit(init)) {
72
+ input = {
73
+ type: 'provider',
74
+ provider: init.shortVideosProvider
75
+ };
76
+ }
77
+ else if (isIdsInit(init)) {
78
+ input = {
79
+ type: 'ids',
80
+ ids: init.ids,
81
+ initialId: init.initialId,
82
+ graphqlUrl: init.graphqlUrl
83
+ };
84
+ }
85
+ else {
86
+ input = null;
87
+ }
88
+ if (!input) {
89
+ return;
90
+ }
26
91
  const shadowHost = new ShadowHost({ onClosed: () => init.on?.playerClosed?.() });
27
92
  mount(ShortVideosPlayer, {
28
93
  target: shadowHost.shadowRoot,
29
94
  props: {
30
- shortVideosProvider,
31
- localization,
95
+ input,
96
+ localization: init.localization,
32
97
  on: {
33
98
  closePlayer: () => {
34
99
  shadowHost.remove();
@@ -0,0 +1,64 @@
1
+ import type * as SchemaTypes from '../../../gql/types';
2
+ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
3
+ export type GetShortVideosQueryVariables = SchemaTypes.Exact<{
4
+ input: SchemaTypes.PostsInput;
5
+ image_scale?: SchemaTypes.InputMaybe<SchemaTypes.ImageScale>;
6
+ }>;
7
+ export type GetShortVideosQuery = {
8
+ shortVideos: {
9
+ items: Array<{
10
+ id: string;
11
+ enableSocialInteractions: boolean;
12
+ postHeading: {
13
+ sourceImage: string | null;
14
+ sourceName: string;
15
+ postDisplayDate: any;
16
+ postViewsCount: number;
17
+ };
18
+ allProducts: Array<{
19
+ title: string;
20
+ id: string;
21
+ link: string | null;
22
+ media: Array<{
23
+ url: string;
24
+ thumbnailUrl: string | null;
25
+ type: SchemaTypes.MediaType;
26
+ }>;
27
+ priceAndAvailability: {
28
+ currency: SchemaTypes.Currency;
29
+ price: number;
30
+ productSalePrices: Array<{
31
+ salePrice: number;
32
+ salePriceEffectiveDateFrom: any | null;
33
+ salePriceEffectiveDateTo: any | null;
34
+ }> | null;
35
+ };
36
+ }>;
37
+ ad: {
38
+ id: string;
39
+ title: string;
40
+ description: string | null;
41
+ buttonText: string | null;
42
+ buttonUrl: string | null;
43
+ openLinkInNewWindow: boolean | null;
44
+ type: SchemaTypes.AdType;
45
+ media: Array<{
46
+ url: string;
47
+ thumbnailUrl: string | null;
48
+ type: SchemaTypes.MediaType;
49
+ }>;
50
+ } | null;
51
+ postData: {
52
+ media: Array<{
53
+ url: string;
54
+ thumbnailUrl: string | null;
55
+ type: SchemaTypes.MediaType;
56
+ }>;
57
+ shortVideoData: {
58
+ text: string | null;
59
+ } | null;
60
+ };
61
+ }>;
62
+ };
63
+ };
64
+ export declare const GetShortVideosDocument: DocumentNode<GetShortVideosQuery, GetShortVideosQueryVariables>;
@@ -0,0 +1,193 @@
1
+ export const GetShortVideosDocument = {
2
+ kind: 'Document',
3
+ definitions: [
4
+ {
5
+ kind: 'OperationDefinition',
6
+ operation: 'query',
7
+ name: { kind: 'Name', value: 'GetShortVideos' },
8
+ variableDefinitions: [
9
+ {
10
+ kind: 'VariableDefinition',
11
+ variable: { kind: 'Variable', name: { kind: 'Name', value: 'input' } },
12
+ type: { kind: 'NonNullType', type: { kind: 'NamedType', name: { kind: 'Name', value: 'PostsInput' } } }
13
+ },
14
+ {
15
+ kind: 'VariableDefinition',
16
+ variable: { kind: 'Variable', name: { kind: 'Name', value: 'image_scale' } },
17
+ type: { kind: 'NamedType', name: { kind: 'Name', value: 'ImageScale' } },
18
+ defaultValue: { kind: 'EnumValue', value: 'ORIGINAL_ENCODED' }
19
+ }
20
+ ],
21
+ selectionSet: {
22
+ kind: 'SelectionSet',
23
+ selections: [
24
+ {
25
+ kind: 'Field',
26
+ alias: { kind: 'Name', value: 'shortVideos' },
27
+ name: { kind: 'Name', value: 'posts' },
28
+ arguments: [{ kind: 'Argument', name: { kind: 'Name', value: 'input' }, value: { kind: 'Variable', name: { kind: 'Name', value: 'input' } } }],
29
+ selectionSet: {
30
+ kind: 'SelectionSet',
31
+ selections: [
32
+ {
33
+ kind: 'Field',
34
+ name: { kind: 'Name', value: 'items' },
35
+ selectionSet: {
36
+ kind: 'SelectionSet',
37
+ selections: [{ kind: 'FragmentSpread', name: { kind: 'Name', value: 'ShortVideoViewerPayloadFragment' } }]
38
+ }
39
+ }
40
+ ]
41
+ }
42
+ }
43
+ ]
44
+ }
45
+ },
46
+ {
47
+ kind: 'FragmentDefinition',
48
+ name: { kind: 'Name', value: 'ShortVideoViewerPayloadFragment' },
49
+ typeCondition: { kind: 'NamedType', name: { kind: 'Name', value: 'Post' } },
50
+ selectionSet: {
51
+ kind: 'SelectionSet',
52
+ selections: [
53
+ { kind: 'Field', name: { kind: 'Name', value: 'id' } },
54
+ { kind: 'Field', name: { kind: 'Name', value: 'enableSocialInteractions' } },
55
+ {
56
+ kind: 'Field',
57
+ name: { kind: 'Name', value: 'postHeading' },
58
+ selectionSet: {
59
+ kind: 'SelectionSet',
60
+ selections: [
61
+ { kind: 'Field', name: { kind: 'Name', value: 'sourceImage' } },
62
+ { kind: 'Field', name: { kind: 'Name', value: 'sourceName' } },
63
+ { kind: 'Field', name: { kind: 'Name', value: 'postDisplayDate' } },
64
+ { kind: 'Field', name: { kind: 'Name', value: 'postViewsCount' } }
65
+ ]
66
+ }
67
+ },
68
+ {
69
+ kind: 'Field',
70
+ name: { kind: 'Name', value: 'allProducts' },
71
+ selectionSet: {
72
+ kind: 'SelectionSet',
73
+ selections: [
74
+ {
75
+ kind: 'Field',
76
+ name: { kind: 'Name', value: 'media' },
77
+ selectionSet: {
78
+ kind: 'SelectionSet',
79
+ selections: [
80
+ { kind: 'Field', name: { kind: 'Name', value: 'url' } },
81
+ { kind: 'Field', name: { kind: 'Name', value: 'thumbnailUrl' } },
82
+ { kind: 'Field', name: { kind: 'Name', value: 'type' } }
83
+ ]
84
+ }
85
+ },
86
+ { kind: 'Field', name: { kind: 'Name', value: 'title' } },
87
+ { kind: 'Field', name: { kind: 'Name', value: 'id' } },
88
+ { kind: 'Field', name: { kind: 'Name', value: 'link' } },
89
+ {
90
+ kind: 'Field',
91
+ name: { kind: 'Name', value: 'priceAndAvailability' },
92
+ selectionSet: {
93
+ kind: 'SelectionSet',
94
+ selections: [
95
+ { kind: 'Field', name: { kind: 'Name', value: 'currency' } },
96
+ { kind: 'Field', name: { kind: 'Name', value: 'price' } },
97
+ {
98
+ kind: 'Field',
99
+ name: { kind: 'Name', value: 'productSalePrices' },
100
+ selectionSet: {
101
+ kind: 'SelectionSet',
102
+ selections: [
103
+ { kind: 'Field', name: { kind: 'Name', value: 'salePrice' } },
104
+ { kind: 'Field', name: { kind: 'Name', value: 'salePriceEffectiveDateFrom' } },
105
+ { kind: 'Field', name: { kind: 'Name', value: 'salePriceEffectiveDateTo' } }
106
+ ]
107
+ }
108
+ }
109
+ ]
110
+ }
111
+ }
112
+ ]
113
+ }
114
+ },
115
+ {
116
+ kind: 'Field',
117
+ name: { kind: 'Name', value: 'ad' },
118
+ selectionSet: {
119
+ kind: 'SelectionSet',
120
+ selections: [
121
+ { kind: 'Field', name: { kind: 'Name', value: 'id' } },
122
+ { kind: 'Field', name: { kind: 'Name', value: 'title' } },
123
+ { kind: 'Field', name: { kind: 'Name', value: 'description' } },
124
+ { kind: 'Field', name: { kind: 'Name', value: 'buttonText' } },
125
+ { kind: 'Field', name: { kind: 'Name', value: 'buttonUrl' } },
126
+ { kind: 'Field', name: { kind: 'Name', value: 'openLinkInNewWindow' } },
127
+ { kind: 'Field', name: { kind: 'Name', value: 'type' } },
128
+ {
129
+ kind: 'Field',
130
+ name: { kind: 'Name', value: 'media' },
131
+ selectionSet: {
132
+ kind: 'SelectionSet',
133
+ selections: [
134
+ { kind: 'Field', name: { kind: 'Name', value: 'url' } },
135
+ { kind: 'Field', name: { kind: 'Name', value: 'thumbnailUrl' } },
136
+ { kind: 'Field', name: { kind: 'Name', value: 'type' } }
137
+ ]
138
+ }
139
+ }
140
+ ]
141
+ }
142
+ },
143
+ {
144
+ kind: 'Field',
145
+ name: { kind: 'Name', value: 'postData' },
146
+ selectionSet: {
147
+ kind: 'SelectionSet',
148
+ selections: [
149
+ {
150
+ kind: 'Field',
151
+ name: { kind: 'Name', value: 'media' },
152
+ selectionSet: {
153
+ kind: 'SelectionSet',
154
+ selections: [
155
+ {
156
+ kind: 'Field',
157
+ name: { kind: 'Name', value: 'url' },
158
+ arguments: [
159
+ {
160
+ kind: 'Argument',
161
+ name: { kind: 'Name', value: 'scale' },
162
+ value: { kind: 'Variable', name: { kind: 'Name', value: 'image_scale' } }
163
+ }
164
+ ]
165
+ },
166
+ {
167
+ kind: 'Field',
168
+ name: { kind: 'Name', value: 'thumbnailUrl' },
169
+ arguments: [
170
+ {
171
+ kind: 'Argument',
172
+ name: { kind: 'Name', value: 'scale' },
173
+ value: { kind: 'Variable', name: { kind: 'Name', value: 'image_scale' } }
174
+ }
175
+ ]
176
+ },
177
+ { kind: 'Field', name: { kind: 'Name', value: 'type' } }
178
+ ]
179
+ }
180
+ },
181
+ {
182
+ kind: 'Field',
183
+ name: { kind: 'Name', value: 'shortVideoData' },
184
+ selectionSet: { kind: 'SelectionSet', selections: [{ kind: 'Field', name: { kind: 'Name', value: 'text' } }] }
185
+ }
186
+ ]
187
+ }
188
+ }
189
+ ]
190
+ }
191
+ }
192
+ ]
193
+ };
@@ -0,0 +1,8 @@
1
+ # noinspection GraphQLSchemaValidation
2
+ query GetShortVideos($input: PostsInput!, $image_scale: ImageScale = ORIGINAL_ENCODED) {
3
+ shortVideos: posts(input: $input) {
4
+ items {
5
+ ...ShortVideoViewerPayloadFragment
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,34 @@
1
+ import type { ShortVideoViewerModel } from '../..';
2
+ import type { IShortVideosPlayerLocalization } from './short-videos-player-localization.svelte';
3
+ import type { PlayerItemsProvider } from '../../ui/player';
4
+ export type PlayerInput = ProviderPlayerInput | IdsPlayerInput;
5
+ export type ProviderPlayerInput = {
6
+ type: 'provider';
7
+ provider: PlayerItemsProvider<ShortVideoViewerModel>;
8
+ };
9
+ export type IdsPlayerInput = {
10
+ type: 'ids';
11
+ ids: string[];
12
+ initialId?: string;
13
+ graphqlUrl: string;
14
+ };
15
+ export type OpenShortVideosPlayerInit = ProviderInit | IdsInit;
16
+ type ProviderInit = {
17
+ shortVideosProvider: PlayerItemsProvider<ShortVideoViewerModel>;
18
+ localization?: IShortVideosPlayerLocalization;
19
+ on?: {
20
+ playerClosed?: () => void;
21
+ };
22
+ };
23
+ type IdsInit = {
24
+ ids: string[];
25
+ graphqlUrl: string;
26
+ initialId?: string;
27
+ localization?: IShortVideosPlayerLocalization;
28
+ on?: {
29
+ playerClosed?: () => void;
30
+ };
31
+ };
32
+ export declare const isShortVideosProviderInit: (init: unknown) => init is ProviderInit;
33
+ export declare function isIdsInit(init: unknown): init is IdsInit;
34
+ export {};
@@ -0,0 +1,6 @@
1
+ export const isShortVideosProviderInit = (init) => {
2
+ return typeof init === 'object' && init !== null && 'shortVideosProvider' in init;
3
+ };
4
+ export function isIdsInit(init) {
5
+ return typeof init === 'object' && init !== null && 'ids' in init && 'graphqlUrl' in init;
6
+ }
@@ -21,14 +21,32 @@ import { default as Overview } from './stream-overview.svelte';
21
21
  import { StreamPlayerBuffer } from './stream-player-buffer.svelte';
22
22
  import { StreamPlayerLocalization } from './stream-player-localization.svelte';
23
23
  import { StreamPlayerUiManager } from './ui-manager.svelte';
24
- import { onDestroy, onMount } from 'svelte';
24
+ import { onMount } from 'svelte';
25
+ import { AppEventsTracker } from '@streamscloud/streams-analytics-collector';
25
26
  let { streamId, graphql, localization: localizationInit, on } = $props();
26
27
  const localization = $derived(new StreamPlayerLocalization(localizationInit));
27
28
  let model = $state(null);
28
29
  let buffer = $state.raw(null);
29
30
  let loading = $state(true);
30
31
  let activePageId = $derived.by(() => { var _a, _b; return (_b = (_a = buffer === null || buffer === void 0 ? void 0 : buffer.current) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : ''; });
32
+ let totalEngagementTimeSeconds = 0;
33
+ let inactiveTimeSeconds = 10; // // Mark as inactive after 10 seconds of no activity
34
+ let isActive = true;
35
+ let activityTimeout = null;
36
+ let trackingInterval = null;
37
+ let maxPageIndexViewed = 0;
31
38
  const uiManager = new StreamPlayerUiManager();
39
+ const resetInactivityTimer = () => {
40
+ if (!isActive) {
41
+ isActive = true;
42
+ }
43
+ if (activityTimeout) {
44
+ clearTimeout(activityTimeout);
45
+ }
46
+ activityTimeout = setTimeout(() => {
47
+ isActive = false;
48
+ }, inactiveTimeSeconds * 1000);
49
+ };
32
50
  onMount(() => __awaiter(void 0, void 0, void 0, function* () {
33
51
  var _a, _b, _c, _d;
34
52
  uiManager.overviewCollapsed = window && window.innerWidth < window.innerHeight;
@@ -40,20 +58,48 @@ onMount(() => __awaiter(void 0, void 0, void 0, function* () {
40
58
  return;
41
59
  }
42
60
  (_c = on === null || on === void 0 ? void 0 : on.streamActivated) === null || _c === void 0 ? void 0 : _c.call(on, {
61
+ ownerId: streamPayload.data.stream.ownerProfile.id,
43
62
  title: streamPayload.data.stream.title,
44
63
  image: ((_d = streamPayload.data.stream.cover) === null || _d === void 0 ? void 0 : _d.url) || null
45
64
  });
46
65
  // start tracking the stream
47
66
  model = mapStreamPlayerModel(streamPayload.data.stream);
48
67
  buffer = new StreamPlayerBuffer({ graphql, streamId });
68
+ AppEventsTracker.trackStreamView(streamPayload.data.stream.id);
69
+ startActivityTracking();
49
70
  }
50
71
  finally {
51
72
  loading = false;
52
73
  }
53
74
  }));
54
- onDestroy(() => {
55
- // end tracking the stream
56
- });
75
+ const startActivityTracking = () => {
76
+ trackingInterval = setInterval(() => {
77
+ if (isActive) {
78
+ totalEngagementTimeSeconds += 1;
79
+ }
80
+ }, 1000);
81
+ window.addEventListener('mousemove', resetInactivityTimer);
82
+ window.addEventListener('mousedown', resetInactivityTimer);
83
+ window.addEventListener('keypress', resetInactivityTimer);
84
+ window.addEventListener('touchstart', resetInactivityTimer);
85
+ window.addEventListener('scroll', resetInactivityTimer);
86
+ resetInactivityTimer();
87
+ };
88
+ const stopActivityTracking = () => {
89
+ window.removeEventListener('mousemove', resetInactivityTimer);
90
+ window.removeEventListener('mousedown', resetInactivityTimer);
91
+ window.removeEventListener('keypress', resetInactivityTimer);
92
+ window.removeEventListener('touchstart', resetInactivityTimer);
93
+ window.removeEventListener('scroll', resetInactivityTimer);
94
+ if (activityTimeout) {
95
+ clearTimeout(activityTimeout);
96
+ activityTimeout = null;
97
+ }
98
+ if (trackingInterval) {
99
+ clearInterval(trackingInterval);
100
+ trackingInterval = null;
101
+ }
102
+ };
57
103
  const handleChangePage = (index) => {
58
104
  if (!buffer) {
59
105
  return;
@@ -69,6 +115,35 @@ const handleDimensionsChanged = (dimensions) => {
69
115
  viewTotalWidth: dimensions.totalWidth
70
116
  });
71
117
  };
118
+ const onPageActivated = (id) => {
119
+ const page = buffer === null || buffer === void 0 ? void 0 : buffer.loaded.find((x) => x.id === id);
120
+ if (page) {
121
+ AppEventsTracker.trackStreamPageView(id, streamId);
122
+ const currentIndex = buffer === null || buffer === void 0 ? void 0 : buffer.loaded.findIndex((p) => p.id === id);
123
+ if (currentIndex !== undefined && currentIndex > maxPageIndexViewed) {
124
+ maxPageIndexViewed = currentIndex;
125
+ }
126
+ }
127
+ };
128
+ const onProductCardClick = (productId) => {
129
+ AppEventsTracker.trackStreamProductClicked(productId, streamId);
130
+ };
131
+ const onPlayerClose = () => {
132
+ var _a;
133
+ stopActivityTracking();
134
+ AppEventsTracker.trackStreamEngagementTime(streamId, totalEngagementTimeSeconds);
135
+ if (buffer && buffer.loaded.length > 0) {
136
+ let scrollDepth = Math.round(((maxPageIndexViewed + 1) / buffer.loaded.length) * 100);
137
+ AppEventsTracker.trackStreamScrollDepth(streamId, scrollDepth);
138
+ }
139
+ (_a = on === null || on === void 0 ? void 0 : on.closePlayer) === null || _a === void 0 ? void 0 : _a.call(on);
140
+ };
141
+ const onPageDeactivated = (itemId) => {
142
+ AppEventsTracker.reportPageVideoViews(itemId, streamId);
143
+ };
144
+ const onProgress = (pageId, videoId, progress) => {
145
+ AppEventsTracker.trackShortVideoProgress(pageId, videoId, progress, streamId);
146
+ };
72
147
  </script>
73
148
 
74
149
  <div class="stream-player-container">
@@ -79,20 +154,25 @@ const handleDimensionsChanged = (dimensions) => {
79
154
  {#if buffer && model}
80
155
  <SpotlightLayout ratio={9 / 16} on={{ dimensionsChanged: handleDimensionsChanged }}>
81
156
  <div class="stream-player__content">
82
- <PlayerSlider buffer={buffer}>
157
+ <PlayerSlider
158
+ buffer={buffer}
159
+ on={{
160
+ itemActivated: (e) => onPageActivated(e),
161
+ itemDeactivated: (e) => onPageDeactivated(e)
162
+ }}>
83
163
  {#snippet children({ item })}
84
164
  {#if item.type === 'general'}
85
165
  <StreamPageViewer
86
166
  page={item}
87
167
  on={{
88
- progress: (videoId: string, progress: number) => on?.progress?.(videoId, progress),
89
- productClick: (productId: string) => on?.productClick?.(productId)
168
+ progress: (videoId: string, progress: number) => onProgress(item.id, videoId, progress),
169
+ productClick: (productId: string) => onProductCardClick(productId)
90
170
  }} />
91
171
  {:else if item.type === 'short-video'}
92
172
  <ShortVideoViewer
93
173
  model={mapToShortVideoViewerModel(item.shortVideo)}
94
174
  on={{
95
- progress: (progress: number) => on?.progress?.(item.id, progress)
175
+ progress: (progress) => onProgress(item.id, item.shortVideo.id, progress)
96
176
  }}
97
177
  autoplay="on-appearance"
98
178
  showAttachments={uiManager.showShortVideoOverlay} />
@@ -115,8 +195,8 @@ const handleDimensionsChanged = (dimensions) => {
115
195
  uiManager={uiManager}
116
196
  localization={localization}
117
197
  on={{
118
- closePlayer: () => on?.closePlayer?.(),
119
- productClick: (productId: string) => on?.productClick?.(productId)
198
+ closePlayer: () => onPlayerClose(),
199
+ productClick: (productId: string) => onProductCardClick(productId)
120
200
  }} />
121
201
  {/if}
122
202
  </div>
@@ -7,11 +7,10 @@ type Props = {
7
7
  on?: {
8
8
  closePlayer?: () => void;
9
9
  streamActivated?: (data: {
10
+ ownerId: string;
10
11
  title: string;
11
12
  image: string | null;
12
13
  }) => void;
13
- productClick?: (productId: string) => void;
14
- progress?: (videoId: string, progress: number) => void;
15
14
  };
16
15
  };
17
16
  declare const Cmp: import("svelte").Component<Props, {}, "">;
@@ -40,7 +40,12 @@ const shortVideo = $derived(((_a = buffer.current) === null || _a === void 0 ? v
40
40
  <div class="stream-player-controls__right">
41
41
  {#if shortVideo && shortVideo.products}
42
42
  <div class="stream-player-controls__short-video-attachments">
43
- <ShortVideoViewerAttachments shortVideo={shortVideo} localization={localization.shortVideoDetailsLocalization?.attachmentsLocalization} />
43
+ <ShortVideoViewerAttachments
44
+ shortVideo={shortVideo}
45
+ localization={localization.shortVideoDetailsLocalization?.attachmentsLocalization}
46
+ on={{
47
+ productClick: on.productClick
48
+ }} />
44
49
  </div>
45
50
  {/if}
46
51
  <div class="stream-player-controls__controls">
@@ -1,7 +1,25 @@
1
1
  import { createLocalGQLClient } from '../../core/graphql';
2
2
  import { ShadowHost } from '../../ui/shadow-dom';
3
3
  import { default as StreamPlayer } from './cmp.stream-player.svelte';
4
+ import { AppEventsTracker } from '@streamscloud/streams-analytics-collector';
4
5
  import { mount } from 'svelte';
6
+ /**
7
+ * Key used for storing the profile ID in local storage
8
+ */
9
+ const PROFILE_ID_STORAGE_KEY = 'streamscloud_profile_id';
10
+ /**
11
+ * Retrieves the profile ID from localStorage or generates a new one if it doesn't exist
12
+ * @returns The profile ID to use for analytics tracking
13
+ */
14
+ function getOrCreateProfileId() {
15
+ const storedProfileId = localStorage.getItem(PROFILE_ID_STORAGE_KEY);
16
+ if (!storedProfileId) {
17
+ const newProfileId = crypto.randomUUID();
18
+ localStorage.setItem(PROFILE_ID_STORAGE_KEY, newProfileId);
19
+ return newProfileId;
20
+ }
21
+ return storedProfileId;
22
+ }
5
23
  /**
6
24
  * Opens the stream player modal with the specified stream details.
7
25
  *
@@ -28,6 +46,8 @@ import { mount } from 'svelte';
28
46
  export const openStreamPlayer = (init) => {
29
47
  const { streamId, graphqlUrl, localization } = init;
30
48
  const shadowHost = new ShadowHost({ onClosed: () => init.on?.playerClosed?.() });
49
+ AppEventsTracker.setEndpoint(graphqlUrl);
50
+ AppEventsTracker.setProfileId(getOrCreateProfileId());
31
51
  mount(StreamPlayer, {
32
52
  target: shadowHost.shadowRoot,
33
53
  props: {
@@ -36,6 +56,7 @@ export const openStreamPlayer = (init) => {
36
56
  localization,
37
57
  on: {
38
58
  streamActivated: (data) => {
59
+ AppEventsTracker.setOrganizationId(data.ownerId);
39
60
  if (init.on?.streamActivated) {
40
61
  init.on.streamActivated({ title: data.title, image: data.image });
41
62
  }
@@ -17,6 +17,8 @@ export type GetStreamQuery = {
17
17
  ownerProfile: {
18
18
  image: string | null;
19
19
  name: string;
20
+ id: string;
21
+ type: SchemaTypes.ProfileType;
20
22
  };
21
23
  availableFrom: {
22
24
  image: string;
@@ -54,6 +56,8 @@ export type StreamPlayerPayloadFragment = {
54
56
  ownerProfile: {
55
57
  image: string | null;
56
58
  name: string;
59
+ id: string;
60
+ type: SchemaTypes.ProfileType;
57
61
  };
58
62
  availableFrom: {
59
63
  image: string;
@@ -25,7 +25,9 @@ export const StreamPlayerPayloadFragmentDoc = {
25
25
  kind: 'SelectionSet',
26
26
  selections: [
27
27
  { kind: 'Field', name: { kind: 'Name', value: 'image' } },
28
- { kind: 'Field', name: { kind: 'Name', value: 'name' } }
28
+ { kind: 'Field', name: { kind: 'Name', value: 'name' } },
29
+ { kind: 'Field', name: { kind: 'Name', value: 'id' } },
30
+ { kind: 'Field', name: { kind: 'Name', value: 'type' } }
29
31
  ]
30
32
  }
31
33
  },
@@ -105,7 +107,9 @@ export const GetStreamDocument = {
105
107
  kind: 'SelectionSet',
106
108
  selections: [
107
109
  { kind: 'Field', name: { kind: 'Name', value: 'image' } },
108
- { kind: 'Field', name: { kind: 'Name', value: 'name' } }
110
+ { kind: 'Field', name: { kind: 'Name', value: 'name' } },
111
+ { kind: 'Field', name: { kind: 'Name', value: 'id' } },
112
+ { kind: 'Field', name: { kind: 'Name', value: 'type' } }
109
113
  ]
110
114
  }
111
115
  },
@@ -26,6 +26,8 @@ fragment StreamPlayerPayloadFragment on Stream {
26
26
  ownerProfile {
27
27
  image
28
28
  name
29
+ id
30
+ type
29
31
  }
30
32
  availableFrom {
31
33
  image
@@ -13,14 +13,16 @@ const fillSvg = (node) => {
13
13
  };
14
14
  </script>
15
15
 
16
- <div class="icon" style="display: contents"
17
- class:icon--white={color === IconColor.White}
18
- class:icon--text={color === IconColor.Text}
19
- class:icon--gray={color === IconColor.Gray}
20
- class:icon--green={color === IconColor.Green}
21
- class:icon--red={color === IconColor.Red}
22
- class:icon--orange={color === IconColor.Orange}
23
- class:icon--blue={color === IconColor.Blue}>
16
+ <div
17
+ class="icon"
18
+ style="display: contents"
19
+ class:icon--white={color === IconColor.White}
20
+ class:icon--text={color === IconColor.Text}
21
+ class:icon--gray={color === IconColor.Gray}
22
+ class:icon--green={color === IconColor.Green}
23
+ class:icon--red={color === IconColor.Red}
24
+ class:icon--orange={color === IconColor.Orange}
25
+ class:icon--blue={color === IconColor.Blue}>
24
26
  {#if src.startsWith('<svg')}
25
27
  {@html src}
26
28
  {:else}
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">import { Utils } from '../../core/utils';
2
2
  import { isScrollingPrevented } from './prevent-slider-scroll';
3
3
  import { onDestroy, onMount } from 'svelte';
4
- let { buffer, children } = $props();
4
+ let { buffer, children, on } = $props();
5
5
  let slidesRef;
6
6
  let sliderHeight = $state(0);
7
7
  let swipeTransition = $state(0);
@@ -17,6 +17,8 @@ const onKeyPress = (e) => {
17
17
  buffer.loadNext();
18
18
  }
19
19
  };
20
+ // Notify the parent component that the active item is about to change.
21
+ let active = null;
20
22
  $effect(() => {
21
23
  if (buffer.currentIndex >= 0 && activeIndex >= 0 && Math.abs(activeIndex - buffer.currentIndex) === 1) {
22
24
  slidesRef.classList.toggle('animate', true);
@@ -85,6 +87,7 @@ onMount(() => {
85
87
  return;
86
88
  }
87
89
  slidesRef.classList.toggle('animate', false);
90
+ notifyOnItemChanged();
88
91
  });
89
92
  sliderHeight = slidesRef.clientHeight;
90
93
  resizeObserver = new ResizeObserver(() => {
@@ -92,6 +95,16 @@ onMount(() => {
92
95
  });
93
96
  resizeObserver.observe(slidesRef);
94
97
  });
98
+ const notifyOnItemChanged = () => {
99
+ var _a, _b, _c;
100
+ if (active && ((_a = buffer.current) === null || _a === void 0 ? void 0 : _a.id) !== active.id) {
101
+ (_b = on === null || on === void 0 ? void 0 : on.itemDeactivated) === null || _b === void 0 ? void 0 : _b.call(on, active.id);
102
+ }
103
+ active = buffer.current;
104
+ if (active) {
105
+ (_c = on === null || on === void 0 ? void 0 : on.itemActivated) === null || _c === void 0 ? void 0 : _c.call(on, active.id);
106
+ }
107
+ };
95
108
  onDestroy(() => {
96
109
  if (resizeObserver) {
97
110
  resizeObserver.disconnect();
@@ -5,6 +5,10 @@ declare class __sveltets_Render<T extends {
5
5
  }> {
6
6
  props(): {
7
7
  buffer: IPlayerBuffer<T>;
8
+ on?: {
9
+ itemDeactivated?: ((e: string) => void) | undefined;
10
+ itemActivated?: ((e: string) => void) | undefined;
11
+ } | undefined;
8
12
  children: Snippet<[{
9
13
  item: T;
10
14
  active?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/embeddable",
3
- "version": "2.1.5",
3
+ "version": "2.2.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": "https://github.com/StreamsCloud/streamscloud-frontend-packages.git",
6
6
  "type": "module",
@@ -95,7 +95,8 @@
95
95
  "@svelte-put/inline-svg": "^4.0.1",
96
96
  "@urql/core": "^5.1.1",
97
97
  "mobile-detect": "^1.4.5",
98
- "svelte": "^5.25.3"
98
+ "svelte": "^5.25.3",
99
+ "@streamscloud/streams-analytics-collector": "latest"
99
100
  },
100
101
  "devDependencies": {
101
102
  "@eslint/compat": "^1.2.9",