Skip to content

Commit 8276fb6

Browse files
authored
Merge pull request #143 from os2display/release/2.2.0
Release 2.2.0
2 parents 6183e12 + 9db0f4a commit 8276fb6

File tree

10 files changed

+293
-27
lines changed

10 files changed

+293
-27
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44

55
## Unreleased
66

7+
## [2.2.0] - 2025-05-09
8+
9+
- [#142](https://github.com/os2display/display-client/pull/142)
10+
- Added support for previews.
11+
712
## [2.1.2] - 2024-11-20
813

914
- [#140](https://github.com/os2display/display-client/pull/140)

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ All endpoint should be configured without a trailing slash. The endpoints `apiEn
3232
left empty if the api is hosted from the root of the same domain as the client. E.g. if the api is at https://example.org and the client is at
3333
https://example.org/client
3434

35+
## Preview
36+
37+
The client can be started in preview mode by setting the following url parameters:
38+
```
39+
preview=<screen|playlist|slide>
40+
preview-id=<id of entity to preview>
41+
preview-token=<token for accessing data>
42+
preview-tenant=<tenant id>
43+
```
44+
45+
The preview will use the token and tenant for acessing the data from the api.
46+
3547
## Docker development setup
3648

3749
Start docker setup

src/app.jsx

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useEffect, useRef, useState } from "react";
2+
import PropTypes from "prop-types";
23
import Screen from "./components/screen";
34
import ContentService from "./service/content-service";
45
import ConfigLoader from "./util/config-loader";
@@ -16,10 +17,13 @@ import constants from "./util/constants";
1617
/**
1718
* App component.
1819
*
20+
* @param {object} props The props.
21+
* @param {string | null} props.preview Type of preview to enable.
22+
* @param {string | null} props.previewId The id of the entity to preview.
1923
* @returns {object}
2024
* The component.
2125
*/
22-
function App() {
26+
function App({ preview, previewId }) {
2327
const [running, setRunning] = useState(false);
2428
const [screen, setScreen] = useState("");
2529
const [bindKey, setBindKey] = useState(null);
@@ -187,30 +191,52 @@ function App() {
187191

188192
useEffect(() => {
189193
logger.info("Mounting App.");
194+
if (preview !== null) {
195+
document.addEventListener("screen", screenHandler);
196+
document.addEventListener("contentEmpty", contentEmpty);
197+
document.addEventListener("contentNotEmpty", contentNotEmpty);
198+
199+
if (preview === "screen") {
200+
startContent(previewId);
201+
return;
202+
}
203+
setRunning(true);
204+
contentServiceRef.current = new ContentService();
205+
contentServiceRef.current.start();
206+
document.dispatchEvent(
207+
new CustomEvent("startPreview", {
208+
detail: {
209+
mode: preview,
210+
id: previewId,
211+
},
212+
})
213+
);
214+
} else {
215+
document.addEventListener("keypress", handleKeyboard);
216+
document.addEventListener("screen", screenHandler);
217+
document.addEventListener("reauthenticate", reauthenticateHandler);
218+
document.addEventListener("contentEmpty", contentEmpty);
219+
document.addEventListener("contentNotEmpty", contentNotEmpty);
190220

191-
document.addEventListener("keypress", handleKeyboard);
192-
document.addEventListener("screen", screenHandler);
193-
document.addEventListener("reauthenticate", reauthenticateHandler);
194-
document.addEventListener("contentEmpty", contentEmpty);
195-
document.addEventListener("contentNotEmpty", contentNotEmpty);
196-
197-
tokenService.checkToken();
221+
tokenService.checkToken();
198222

199-
ConfigLoader.loadConfig().then((config) => {
200-
setDebug(config.debug ?? false);
201-
});
223+
ConfigLoader.loadConfig().then((config) => {
224+
setDebug(config.debug ?? false);
225+
});
202226

203-
releaseService.checkForNewRelease().finally(() => {
204-
releaseService.setPreviousBootInUrl();
205-
releaseService.startReleaseCheck();
227+
releaseService.checkForNewRelease().finally(() => {
228+
releaseService.setPreviousBootInUrl();
229+
releaseService.startReleaseCheck();
206230

207-
checkLogin();
231+
checkLogin();
208232

209-
appStorage.setPreviousBoot(new Date().getTime());
210-
});
233+
appStorage.setPreviousBoot(new Date().getTime());
234+
});
211235

212-
statusService.setStatusInUrl();
236+
statusService.setStatusInUrl();
237+
}
213238

239+
/* eslint-disable-next-line consistent-return */
214240
return function cleanup() {
215241
logger.info("Unmounting App.");
216242

@@ -261,4 +287,9 @@ function App() {
261287
);
262288
}
263289

290+
App.propTypes = {
291+
preview: PropTypes.string,
292+
previewId: PropTypes.string,
293+
};
294+
264295
export default App;

src/data-sync/api-helper.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,25 @@ class ApiHelper {
2727
let response;
2828

2929
try {
30-
logger.info(`Fetching: ${this.endpoint + path}`);
30+
const url = new URL(window.location.href);
31+
const previewToken = url.searchParams.get('preview-token');
32+
const previewTenant = url.searchParams.get('preview-tenant');
33+
34+
logger.log('info', `Fetching: ${this.endpoint + path}`);
3135

3236
const token = appStorage.getToken();
3337
const tenantKey = appStorage.getTenantKey();
3438

35-
if (!token || !tenantKey) {
39+
if ((!token || !tenantKey) && (!previewToken || !previewTenant)) {
3640
logger.error('Token or tenantKey not set.');
3741

3842
return null;
3943
}
4044

4145
response = await fetch(this.endpoint + path, {
4246
headers: {
43-
authorization: `Bearer ${token}`,
44-
'Authorization-Tenant-Key': tenantKey,
47+
authorization: `Bearer ${previewToken ?? token}`,
48+
'Authorization-Tenant-Key': previewTenant ?? tenantKey,
4549
},
4650
});
4751

src/data-sync/pull-strategy.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,40 @@ class PullStrategy {
408408
document.dispatchEvent(event);
409409
}
410410

411+
getPath(id) {
412+
return this.apiHelper.getPath(id);
413+
}
414+
415+
async getTemplateData(slide) {
416+
return new Promise((resolve) => {
417+
const templatePath = slide.templateInfo['@id'];
418+
419+
this.apiHelper.getPath(templatePath).then((data) => {
420+
resolve(data);
421+
});
422+
});
423+
}
424+
425+
async getFeedData(slide) {
426+
return new Promise((resolve) => {
427+
if (!slide?.feed?.feedUrl) {
428+
resolve([]);
429+
} else {
430+
this.apiHelper.getPath(slide.feed.feedUrl).then((data) => {
431+
resolve(data);
432+
});
433+
}
434+
});
435+
}
436+
437+
async getMediaData(media) {
438+
return new Promise((resolve) => {
439+
this.apiHelper.getPath(media).then((data) => {
440+
resolve(data);
441+
});
442+
});
443+
}
444+
411445
/**
412446
* Start the data synchronization.
413447
*/

src/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
import { createRoot } from 'react-dom/client';
3+
import App from './app';
4+
5+
const url = new URL(window.location.href);
6+
const preview = url.searchParams.get('preview');
7+
const previewId = url.searchParams.get('preview-id');
8+
9+
const container = document.getElementById('root');
10+
const root = createRoot(container);
11+
12+
root.render(<App preview={preview} previewId={previewId} />);

src/index.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import React from "react";
22
import { createRoot } from "react-dom/client";
33
import App from "./app";
44

5+
const url = new URL(window.location.href);
6+
const preview = url.searchParams.get("preview");
7+
const previewId = url.searchParams.get("preview-id");
8+
59
const container = document.getElementById("root");
610
const root = createRoot(container);
711

8-
root.render(<App />);
12+
root.render(<App preview={preview} previewId={previewId} />);

src/service/content-service.js

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import sha256 from 'crypto-js/sha256';
22
import Base64 from 'crypto-js/enc-base64';
3+
import PullStrategy from '../data-sync/pull-strategy';
4+
import {
5+
screenForPlaylistPreview,
6+
screenForSlidePreview,
7+
} from '../util/preview';
38
import logger from '../logger/logger';
49
import DataSync from '../data-sync/data-sync';
510
import ScheduleService from './schedule-service';
@@ -81,14 +86,17 @@ class ContentService {
8186

8287
this.stopSyncHandler();
8388

89+
logger.log(
90+
'info',
91+
`Event received: Start data synchronization from ${data?.screenPath}`
92+
);
8493
if (data?.screenPath) {
8594
logger.info(
8695
`Event received: Start data synchronization from ${data.screenPath}`
8796
);
8897
this.startSyncing(data.screenPath);
8998
} else {
90-
logger.info('Event received: Start data synchronization');
91-
this.startSyncing();
99+
logger.log('error', 'Error: screenPath not set.');
92100
}
93101
}
94102

@@ -176,6 +184,7 @@ class ContentService {
176184
document.addEventListener('content', this.contentHandler);
177185
document.addEventListener('regionReady', this.regionReadyHandler);
178186
document.addEventListener('regionRemoved', this.regionRemovedHandler);
187+
document.addEventListener('startPreview', this.startPreview);
179188
}
180189

181190
/**
@@ -189,6 +198,93 @@ class ContentService {
189198
document.removeEventListener('content', this.contentHandler);
190199
document.removeEventListener('regionReady', this.regionReadyHandler);
191200
document.removeEventListener('regionRemoved', this.regionRemovedHandler);
201+
document.removeEventListener('startPreview', this.startPreview);
202+
}
203+
204+
/**
205+
* Start preview.
206+
*
207+
* @param {CustomEvent} event The event.
208+
*/
209+
async startPreview(event) {
210+
const data = event.detail;
211+
const { mode, id } = data;
212+
logger.log('info', `Starting preview. Mode: ${mode}, ID: ${id}`);
213+
214+
const config = await ConfigLoader.loadConfig();
215+
216+
if (mode === 'screen') {
217+
this.startSyncing(`/v2/screen/${id}`);
218+
} else if (mode === 'playlist') {
219+
const pullStrategy = new PullStrategy({
220+
endpoint: config.apiEndpoint,
221+
});
222+
223+
const playlist = await pullStrategy.getPath(`/v2/playlists/${id}`);
224+
225+
const playlistSlidesResponse = await pullStrategy.getPath(
226+
playlist.slides
227+
);
228+
229+
playlist.slidesData = playlistSlidesResponse['hydra:member'].map(
230+
(playlistSlide) => playlistSlide.slide
231+
);
232+
233+
// eslint-disable-next-line no-restricted-syntax
234+
for (const slide of playlist.slidesData) {
235+
// eslint-disable-next-line no-await-in-loop
236+
await ContentService.attachReferencesToSlide(pullStrategy, slide);
237+
}
238+
239+
const screen = screenForPlaylistPreview(playlist);
240+
241+
document.dispatchEvent(
242+
new CustomEvent('content', {
243+
detail: {
244+
screen,
245+
},
246+
})
247+
);
248+
} else if (mode === 'slide') {
249+
const pullStrategy = new PullStrategy({
250+
endpoint: config.apiEndpoint,
251+
});
252+
253+
const slide = await pullStrategy.getPath(`/v2/slides/${id}`);
254+
255+
// eslint-disable-next-line no-await-in-loop
256+
await ContentService.attachReferencesToSlide(pullStrategy, slide);
257+
258+
const screen = screenForSlidePreview(slide);
259+
260+
document.dispatchEvent(
261+
new CustomEvent('content', {
262+
detail: {
263+
screen,
264+
},
265+
})
266+
);
267+
} else {
268+
logger.error(`Unsupported preview mode: ${mode}.`);
269+
}
270+
}
271+
272+
static async attachReferencesToSlide(strategy, slide) {
273+
/* eslint-disable no-param-reassign */
274+
slide.templateData = await strategy.getTemplateData(slide);
275+
slide.feedData = await strategy.getFeedData(slide);
276+
277+
slide.mediaData = {};
278+
// eslint-disable-next-line no-restricted-syntax
279+
for (const media of slide.media) {
280+
// eslint-disable-next-line no-await-in-loop
281+
slide.mediaData[media] = await strategy.getMediaData(media);
282+
}
283+
284+
if (typeof slide.theme === 'string' || slide.theme instanceof String) {
285+
slide.theme = await strategy.getPath(slide.theme);
286+
}
287+
/* eslint-enable no-param-reassign */
192288
}
193289

194290
/**

src/service/token-service.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,19 @@ class TokenService {
132132
checkToken = () => {
133133
const expiredState = this.getExpireState();
134134

135-
if ([constants.NO_EXPIRE, constants.NO_ISSUED_AT, constants.NO_TOKEN].includes(expiredState)) {
135+
if (
136+
[
137+
constants.NO_EXPIRE,
138+
constants.NO_ISSUED_AT,
139+
constants.NO_TOKEN,
140+
].includes(expiredState)
141+
) {
136142
// Ignore. No token saved in storage.
137143
} else if (expiredState === constants.TOKEN_EXPIRED) {
138144
statusService.setError(constants.ERROR_TOKEN_EXPIRED);
139-
} else if (expiredState === constants.TOKEN_VALID_SHOULD_HAVE_BEEN_REFRESHED) {
145+
} else if (
146+
expiredState === constants.TOKEN_VALID_SHOULD_HAVE_BEEN_REFRESHED
147+
) {
140148
statusService.setError(
141149
constants.ERROR_TOKEN_VALID_SHOULD_HAVE_BEEN_REFRESHED
142150
);

0 commit comments

Comments
 (0)