@openbrf/display 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/LICENSE-EXCEPTION.md +28 -0
- package/README.md +126 -2
- package/dist/contract.d.ts +244 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +54 -0
- package/dist/contract.js.map +1 -0
- package/dist/fetch-loop.d.ts +87 -0
- package/dist/fetch-loop.d.ts.map +1 -0
- package/dist/fetch-loop.js +351 -0
- package/dist/fetch-loop.js.map +1 -0
- package/dist/fit.d.ts +44 -0
- package/dist/fit.d.ts.map +1 -0
- package/dist/fit.js +257 -0
- package/dist/fit.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/parse.d.ts +39 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +494 -0
- package/dist/parse.js.map +1 -0
- package/dist/renderer.d.ts +85 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +476 -0
- package/dist/renderer.js.map +1 -0
- package/dist/rotation.d.ts +129 -0
- package/dist/rotation.d.ts.map +1 -0
- package/dist/rotation.js +335 -0
- package/dist/rotation.js.map +1 -0
- package/dist/slides.d.ts +41 -0
- package/dist/slides.d.ts.map +1 -0
- package/dist/slides.js +341 -0
- package/dist/slides.js.map +1 -0
- package/dist/styles.d.ts +10 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +107 -0
- package/dist/styles.js.map +1 -0
- package/dist/text.d.ts +16 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +62 -0
- package/dist/text.js.map +1 -0
- package/dist/time.d.ts +39 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +94 -0
- package/dist/time.js.map +1 -0
- package/dist/translate.d.ts +18 -0
- package/dist/translate.d.ts.map +1 -0
- package/dist/translate.js +35 -0
- package/dist/translate.js.map +1 -0
- package/dist/words.d.ts +41 -0
- package/dist/words.d.ts.map +1 -0
- package/dist/words.js +73 -0
- package/dist/words.js.map +1 -0
- package/package.json +58 -3
- package/src/contract.ts +279 -0
- package/src/fetch-loop.ts +472 -0
- package/src/fit.ts +348 -0
- package/src/index.ts +77 -0
- package/src/parse.ts +605 -0
- package/src/renderer.ts +708 -0
- package/src/rotation.ts +515 -0
- package/src/slides.ts +595 -0
- package/src/styles.ts +113 -0
- package/src/text.ts +65 -0
- package/src/time.ts +147 -0
- package/src/translate.ts +56 -0
- package/src/words.ts +91 -0
package/dist/time.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const MINUTE = 60 * 1000;
|
|
2
|
+
const DAY = 24 * 60 * MINUTE;
|
|
3
|
+
const partFormatters = new Map();
|
|
4
|
+
function partFormatter(timeZone) {
|
|
5
|
+
let formatter = partFormatters.get(timeZone);
|
|
6
|
+
if (formatter === undefined) {
|
|
7
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
8
|
+
timeZone,
|
|
9
|
+
hourCycle: "h23",
|
|
10
|
+
year: "numeric",
|
|
11
|
+
month: "numeric",
|
|
12
|
+
day: "numeric",
|
|
13
|
+
hour: "numeric",
|
|
14
|
+
minute: "numeric",
|
|
15
|
+
second: "numeric",
|
|
16
|
+
});
|
|
17
|
+
partFormatters.set(timeZone, formatter);
|
|
18
|
+
}
|
|
19
|
+
return formatter;
|
|
20
|
+
}
|
|
21
|
+
/** Whether `Intl` knows the zone. */
|
|
22
|
+
export function isTimeZone(timeZone) {
|
|
23
|
+
try {
|
|
24
|
+
partFormatter(timeZone);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The wall clock of `timeZone` at an instant. */
|
|
32
|
+
export function zonedParts(time, timeZone) {
|
|
33
|
+
const values = {};
|
|
34
|
+
for (const part of partFormatter(timeZone).formatToParts(time)) {
|
|
35
|
+
if (part.type !== "literal") {
|
|
36
|
+
values[part.type] = Number(part.value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const year = values["year"] ?? 1970;
|
|
40
|
+
const month = values["month"] ?? 1;
|
|
41
|
+
const day = values["day"] ?? 1;
|
|
42
|
+
return {
|
|
43
|
+
year,
|
|
44
|
+
month,
|
|
45
|
+
day,
|
|
46
|
+
hour: (values["hour"] ?? 0) % 24,
|
|
47
|
+
minute: values["minute"] ?? 0,
|
|
48
|
+
second: values["second"] ?? 0,
|
|
49
|
+
weekday: isoWeekday(year, month, day),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function isoWeekday(year, month, day) {
|
|
53
|
+
const weekday = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
|
|
54
|
+
return (weekday === 0 ? 7 : weekday);
|
|
55
|
+
}
|
|
56
|
+
/** How far the zone's wall clock is ahead of UTC at an instant, in ms. */
|
|
57
|
+
function offset(time, timeZone) {
|
|
58
|
+
const parts = zonedParts(time, timeZone);
|
|
59
|
+
const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
60
|
+
return wall - Math.floor(time / 1000) * 1000;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The first instant of a calendar date in `timeZone`: its midnight, or, in a
|
|
64
|
+
* zone that skips midnight on the day the clocks go forward, the moment the
|
|
65
|
+
* day begins.
|
|
66
|
+
*/
|
|
67
|
+
export function zonedDayStart(year, month, day, timeZone) {
|
|
68
|
+
const wall = Date.UTC(year, month - 1, day);
|
|
69
|
+
const first = wall - offset(wall, timeZone);
|
|
70
|
+
const second = wall - offset(first, timeZone);
|
|
71
|
+
const onTheDay = [first, second]
|
|
72
|
+
.filter((candidate) => {
|
|
73
|
+
const parts = zonedParts(candidate, timeZone);
|
|
74
|
+
return parts.year === year && parts.month === month && parts.day === day;
|
|
75
|
+
})
|
|
76
|
+
.sort((a, b) => a - b);
|
|
77
|
+
return onTheDay[0] ?? Math.max(first, second);
|
|
78
|
+
}
|
|
79
|
+
/** The first instant of the calendar day `days` after the one `time` falls on. */
|
|
80
|
+
export function zonedDayStartAfter(time, days, timeZone) {
|
|
81
|
+
const today = zonedParts(time, timeZone);
|
|
82
|
+
const date = new Date(Date.UTC(today.year, today.month - 1, today.day + days));
|
|
83
|
+
return zonedDayStart(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), timeZone);
|
|
84
|
+
}
|
|
85
|
+
/** An instant floored to its minute. */
|
|
86
|
+
export function floorToMinute(time) {
|
|
87
|
+
return Math.floor(time / MINUTE) * MINUTE;
|
|
88
|
+
}
|
|
89
|
+
/** An instant as the contract writes one: UTC, whole seconds, "Z". */
|
|
90
|
+
export function toInstant(time) {
|
|
91
|
+
return `${new Date(Math.floor(time / 1000) * 1000).toISOString().slice(0, 19)}Z`;
|
|
92
|
+
}
|
|
93
|
+
export { DAY, MINUTE };
|
|
94
|
+
//# sourceMappingURL=time.js.map
|
package/dist/time.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time.js","sourceRoot":"","sources":["../src/time.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer's words in a screen's locale, with i18next's interpolation and
|
|
3
|
+
* plural keys, so the translations read the same here as in packages/i18n.
|
|
4
|
+
*/
|
|
5
|
+
import type { ScreenLocale } from "./contract.ts";
|
|
6
|
+
import { type WordKey } from "./words.ts";
|
|
7
|
+
type PluralCategory = "zero" | "one" | "two" | "few" | "many" | "other";
|
|
8
|
+
type PluralBase<Key> = Key extends `${infer Base}_other` ? Base : never;
|
|
9
|
+
/** A word to look up: a key, or the base of a plural key such as `placesLeft`. */
|
|
10
|
+
export type Phrase = Exclude<WordKey, `${string}_${PluralCategory}`> | PluralBase<WordKey>;
|
|
11
|
+
/**
|
|
12
|
+
* The word for `phrase` in `locale`, its `{{name}}` placeholders filled from
|
|
13
|
+
* `values`. A numeric `count` picks the plural form, `<phrase>_one` or
|
|
14
|
+
* `<phrase>_other`, by the locale's plural rules.
|
|
15
|
+
*/
|
|
16
|
+
export declare function translate(locale: ScreenLocale, phrase: Phrase, values?: Readonly<Record<string, string | number>>): string;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=translate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAS,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAEjD,KAAK,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAExE,KAAK,UAAU,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,IAAI,QAAQ,GAAG,IAAI,GAAG,KAAK,CAAC;AAExE,kFAAkF;AAClF,MAAM,MAAM,MAAM,GAChB,OAAO,CAAC,OAAO,EAAE,GAAG,MAAM,IAAI,cAAc,EAAE,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;AAoBxE;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAM,GACrD,MAAM,CAaR"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { WORDS } from "./words.js";
|
|
2
|
+
const pluralRules = new Map();
|
|
3
|
+
function pluralCategory(locale, count) {
|
|
4
|
+
let rules = pluralRules.get(locale);
|
|
5
|
+
if (rules === undefined) {
|
|
6
|
+
rules = new Intl.PluralRules(locale);
|
|
7
|
+
pluralRules.set(locale, rules);
|
|
8
|
+
}
|
|
9
|
+
return rules.select(count);
|
|
10
|
+
}
|
|
11
|
+
function lookup(locale, key) {
|
|
12
|
+
const words = WORDS[locale];
|
|
13
|
+
return Object.prototype.hasOwnProperty.call(words, key)
|
|
14
|
+
? words[key]
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The word for `phrase` in `locale`, its `{{name}}` placeholders filled from
|
|
19
|
+
* `values`. A numeric `count` picks the plural form, `<phrase>_one` or
|
|
20
|
+
* `<phrase>_other`, by the locale's plural rules.
|
|
21
|
+
*/
|
|
22
|
+
export function translate(locale, phrase, values = {}) {
|
|
23
|
+
const count = values["count"];
|
|
24
|
+
const text = (typeof count === "number"
|
|
25
|
+
? (lookup(locale, `${phrase}_${pluralCategory(locale, count)}`) ??
|
|
26
|
+
lookup(locale, `${phrase}_other`))
|
|
27
|
+
: undefined) ??
|
|
28
|
+
lookup(locale, phrase) ??
|
|
29
|
+
phrase;
|
|
30
|
+
return text.replace(/\{\{\s*(\w+)\s*\}\}/g, (placeholder, name) => {
|
|
31
|
+
const value = values[name];
|
|
32
|
+
return value === undefined ? placeholder : String(value);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=translate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate.js","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAgB,MAAM,YAAY,CAAC;AAUjD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkC,CAAC;AAE9D,SAAS,cAAc,CAAC,MAAoB,EAAE,KAAa;IACzD,IAAI,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,KAAK,GAAG,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACrC,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,MAAM,CAAC,MAAoB,EAAE,GAAW;IAC/C,MAAM,KAAK,GAAqC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9D,OAAO,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC;QACrD,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;QACZ,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CACvB,MAAoB,EACpB,MAAc,EACd,MAAM,GAA8C,EAAE;IAEtD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,IAAI,GACR,CAAC,OAAO,KAAK,KAAK,QAAQ;QACxB,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,MAAM,IAAI,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC;YAC7D,MAAM,CAAC,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,CAAC;QACpC,CAAC,CAAC,SAAS,CAAC;QACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;QACtB,MAAM,CAAC;IACT,OAAO,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC,WAAW,EAAE,IAAY,EAAE,EAAE;QACxE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3B,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/words.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ScreenLocale } from "./contract.ts";
|
|
2
|
+
declare const en: {
|
|
3
|
+
readonly loading: "Loading";
|
|
4
|
+
readonly comingEvents: "Coming events";
|
|
5
|
+
readonly calledOff: "Called off";
|
|
6
|
+
readonly placesLeft_one: "{{count}} place left";
|
|
7
|
+
readonly placesLeft_other: "{{count}} places left";
|
|
8
|
+
readonly fullyBooked: "Fully booked";
|
|
9
|
+
readonly ordinaryMeeting: "Ordinary general meeting";
|
|
10
|
+
readonly extraordinaryMeeting: "Extraordinary general meeting";
|
|
11
|
+
readonly meetingWhen: "When";
|
|
12
|
+
readonly meetingWhere: "Where";
|
|
13
|
+
readonly digitalParticipation: "Taking part digitally";
|
|
14
|
+
readonly agenda: "Agenda";
|
|
15
|
+
readonly freeAndBusy: "Free and busy";
|
|
16
|
+
readonly today: "Today";
|
|
17
|
+
readonly tomorrow: "Tomorrow";
|
|
18
|
+
readonly busy: "Busy";
|
|
19
|
+
readonly busyAllDay: "Busy all day";
|
|
20
|
+
readonly freeAllDay: "Free all day";
|
|
21
|
+
readonly freeRestOfDay: "Free for the rest of the day";
|
|
22
|
+
readonly freeBetween: "Free {{from}}–{{to}}";
|
|
23
|
+
readonly timeRange: "{{from}}–{{to}}";
|
|
24
|
+
readonly floor: "Floor {{floor}}";
|
|
25
|
+
readonly nothingToShow: "Nothing to show right now";
|
|
26
|
+
readonly lastUpdated: "Last updated {{time}}";
|
|
27
|
+
readonly notConnected: "Not connected";
|
|
28
|
+
readonly notConnectedDetail: "The screen has no connection just now. It keeps trying by itself.";
|
|
29
|
+
readonly linkInvalid: "This display link is no longer valid";
|
|
30
|
+
readonly linkInvalidDetail: "Ask the board for a new display link and open it on this screen.";
|
|
31
|
+
readonly linkMissing: "The display link is not complete";
|
|
32
|
+
readonly linkMissingDetail: "Open the whole display link from the board on this screen, everything after the # sign included.";
|
|
33
|
+
readonly paused: "The screen is paused";
|
|
34
|
+
readonly pausedDetail: "Screen content is switched off for now. The screen starts again by itself when it is switched back on.";
|
|
35
|
+
};
|
|
36
|
+
/** A word the renderer shows that is not in the document. */
|
|
37
|
+
export type WordKey = keyof typeof en;
|
|
38
|
+
/** The renderer's own words in every screen language, English the canonical one. */
|
|
39
|
+
export declare const WORDS: Readonly<Record<ScreenLocale, Readonly<Record<WordKey, string>>>>;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=words.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"words.d.ts","sourceRoot":"","sources":["../src/words.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,QAAA,MAAM,EAAE;aACN,OAAO,EAAE,SAAS;aAClB,YAAY,EAAE,eAAe;aAC7B,SAAS,EAAE,YAAY;aACvB,cAAc,EAAE,sBAAsB;aACtC,gBAAgB,EAAE,uBAAuB;aACzC,WAAW,EAAE,cAAc;aAC3B,eAAe,EAAE,0BAA0B;aAC3C,oBAAoB,EAAE,+BAA+B;aACrD,WAAW,EAAE,MAAM;aACnB,YAAY,EAAE,OAAO;aACrB,oBAAoB,EAAE,uBAAuB;aAC7C,MAAM,EAAE,QAAQ;aAChB,WAAW,EAAE,eAAe;aAC5B,KAAK,EAAE,OAAO;aACd,QAAQ,EAAE,UAAU;aACpB,IAAI,EAAE,MAAM;aACZ,UAAU,EAAE,cAAc;aAC1B,UAAU,EAAE,cAAc;aAC1B,aAAa,EAAE,8BAA8B;aAC7C,WAAW,EAAE,sBAAsB;aACnC,SAAS,EAAE,iBAAiB;aAC5B,KAAK,EAAE,iBAAiB;aACxB,aAAa,EAAE,2BAA2B;aAC1C,WAAW,EAAE,uBAAuB;aACpC,YAAY,EAAE,eAAe;aAC7B,kBAAkB,EAChB,mEAAmE;aACrE,WAAW,EAAE,sCAAsC;aACnD,iBAAiB,EACf,kEAAkE;aACpE,WAAW,EAAE,kCAAkC;aAC/C,iBAAiB,EACf,kGAAkG;aACpG,MAAM,EAAE,sBAAsB;aAC9B,YAAY,EACV,wGAAwG;CAClG,CAAC;AAEX,6DAA6D;AAC7D,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,EAAE,CAAC;AAEtC,oFAAoF;AACpF,eAAO,MAAM,KAAK,EAAE,QAAQ,CAC1B,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAyCxD,CAAC"}
|
package/dist/words.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const en = {
|
|
2
|
+
loading: "Loading",
|
|
3
|
+
comingEvents: "Coming events",
|
|
4
|
+
calledOff: "Called off",
|
|
5
|
+
placesLeft_one: "{{count}} place left",
|
|
6
|
+
placesLeft_other: "{{count}} places left",
|
|
7
|
+
fullyBooked: "Fully booked",
|
|
8
|
+
ordinaryMeeting: "Ordinary general meeting",
|
|
9
|
+
extraordinaryMeeting: "Extraordinary general meeting",
|
|
10
|
+
meetingWhen: "When",
|
|
11
|
+
meetingWhere: "Where",
|
|
12
|
+
digitalParticipation: "Taking part digitally",
|
|
13
|
+
agenda: "Agenda",
|
|
14
|
+
freeAndBusy: "Free and busy",
|
|
15
|
+
today: "Today",
|
|
16
|
+
tomorrow: "Tomorrow",
|
|
17
|
+
busy: "Busy",
|
|
18
|
+
busyAllDay: "Busy all day",
|
|
19
|
+
freeAllDay: "Free all day",
|
|
20
|
+
freeRestOfDay: "Free for the rest of the day",
|
|
21
|
+
freeBetween: "Free {{from}}–{{to}}",
|
|
22
|
+
timeRange: "{{from}}–{{to}}",
|
|
23
|
+
floor: "Floor {{floor}}",
|
|
24
|
+
nothingToShow: "Nothing to show right now",
|
|
25
|
+
lastUpdated: "Last updated {{time}}",
|
|
26
|
+
notConnected: "Not connected",
|
|
27
|
+
notConnectedDetail: "The screen has no connection just now. It keeps trying by itself.",
|
|
28
|
+
linkInvalid: "This display link is no longer valid",
|
|
29
|
+
linkInvalidDetail: "Ask the board for a new display link and open it on this screen.",
|
|
30
|
+
linkMissing: "The display link is not complete",
|
|
31
|
+
linkMissingDetail: "Open the whole display link from the board on this screen, everything after the # sign included.",
|
|
32
|
+
paused: "The screen is paused",
|
|
33
|
+
pausedDetail: "Screen content is switched off for now. The screen starts again by itself when it is switched back on.",
|
|
34
|
+
};
|
|
35
|
+
/** The renderer's own words in every screen language, English the canonical one. */
|
|
36
|
+
export const WORDS = {
|
|
37
|
+
en,
|
|
38
|
+
sv: {
|
|
39
|
+
loading: "Laddar",
|
|
40
|
+
comingEvents: "Kommande evenemang",
|
|
41
|
+
calledOff: "Inställt",
|
|
42
|
+
placesLeft_one: "{{count}} plats kvar",
|
|
43
|
+
placesLeft_other: "{{count}} platser kvar",
|
|
44
|
+
fullyBooked: "Fullbokat",
|
|
45
|
+
ordinaryMeeting: "Ordinarie föreningsstämma",
|
|
46
|
+
extraordinaryMeeting: "Extra föreningsstämma",
|
|
47
|
+
meetingWhen: "När",
|
|
48
|
+
meetingWhere: "Var",
|
|
49
|
+
digitalParticipation: "Delta digitalt",
|
|
50
|
+
agenda: "Dagordning",
|
|
51
|
+
freeAndBusy: "Ledigt och bokat",
|
|
52
|
+
today: "I dag",
|
|
53
|
+
tomorrow: "I morgon",
|
|
54
|
+
busy: "Bokat",
|
|
55
|
+
busyAllDay: "Bokat hela dagen",
|
|
56
|
+
freeAllDay: "Ledigt hela dagen",
|
|
57
|
+
freeRestOfDay: "Ledigt resten av dagen",
|
|
58
|
+
freeBetween: "Ledigt {{from}}–{{to}}",
|
|
59
|
+
timeRange: "{{from}}–{{to}}",
|
|
60
|
+
floor: "Våning {{floor}}",
|
|
61
|
+
nothingToShow: "Inget att visa just nu",
|
|
62
|
+
lastUpdated: "Senast uppdaterad {{time}}",
|
|
63
|
+
notConnected: "Inte ansluten",
|
|
64
|
+
notConnectedDetail: "Skärmen har ingen anslutning just nu. Den fortsätter att försöka av sig själv.",
|
|
65
|
+
linkInvalid: "Visningslänken gäller inte längre",
|
|
66
|
+
linkInvalidDetail: "Be styrelsen om en ny visningslänk och öppna den på den här skärmen.",
|
|
67
|
+
linkMissing: "Visningslänken är inte hel",
|
|
68
|
+
linkMissingDetail: "Öppna hela visningslänken från styrelsen på den här skärmen, även allt efter tecknet #.",
|
|
69
|
+
paused: "Skärmen är pausad",
|
|
70
|
+
pausedDetail: "Skärminnehållet är avstängt just nu. Skärmen startar igen av sig själv när det slås på.",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=words.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"words.js","sourceRoot":"","sources":["../src/words.ts"],"names":[],"mappings":"AAKA,MAAM,EAAE,GAAG;IACT,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,eAAe;IAC7B,SAAS,EAAE,YAAY;IACvB,cAAc,EAAE,sBAAsB;IACtC,gBAAgB,EAAE,uBAAuB;IACzC,WAAW,EAAE,cAAc;IAC3B,eAAe,EAAE,0BAA0B;IAC3C,oBAAoB,EAAE,+BAA+B;IACrD,WAAW,EAAE,MAAM;IACnB,YAAY,EAAE,OAAO;IACrB,oBAAoB,EAAE,uBAAuB;IAC7C,MAAM,EAAE,QAAQ;IAChB,WAAW,EAAE,eAAe;IAC5B,KAAK,EAAE,OAAO;IACd,QAAQ,EAAE,UAAU;IACpB,IAAI,EAAE,MAAM;IACZ,UAAU,EAAE,cAAc;IAC1B,UAAU,EAAE,cAAc;IAC1B,aAAa,EAAE,8BAA8B;IAC7C,WAAW,EAAE,sBAAsB;IACnC,SAAS,EAAE,iBAAiB;IAC5B,KAAK,EAAE,iBAAiB;IACxB,aAAa,EAAE,2BAA2B;IAC1C,WAAW,EAAE,uBAAuB;IACpC,YAAY,EAAE,eAAe;IAC7B,kBAAkB,EAChB,mEAAmE;IACrE,WAAW,EAAE,sCAAsC;IACnD,iBAAiB,EACf,kEAAkE;IACpE,WAAW,EAAE,kCAAkC;IAC/C,iBAAiB,EACf,kGAAkG;IACpG,MAAM,EAAE,sBAAsB;IAC9B,YAAY,EACV,wGAAwG;CAClG,CAAC;AAKX,oFAAoF;AACpF,MAAM,CAAC,MAAM,KAAK,GAEd;IACF,EAAE;IACF,EAAE,EAAE;QACF,OAAO,EAAE,QAAQ;QACjB,YAAY,EAAE,oBAAoB;QAClC,SAAS,EAAE,UAAU;QACrB,cAAc,EAAE,sBAAsB;QACtC,gBAAgB,EAAE,wBAAwB;QAC1C,WAAW,EAAE,WAAW;QACxB,eAAe,EAAE,2BAA2B;QAC5C,oBAAoB,EAAE,uBAAuB;QAC7C,WAAW,EAAE,KAAK;QAClB,YAAY,EAAE,KAAK;QACnB,oBAAoB,EAAE,gBAAgB;QACtC,MAAM,EAAE,YAAY;QACpB,WAAW,EAAE,kBAAkB;QAC/B,KAAK,EAAE,OAAO;QACd,QAAQ,EAAE,UAAU;QACpB,IAAI,EAAE,OAAO;QACb,UAAU,EAAE,kBAAkB;QAC9B,UAAU,EAAE,mBAAmB;QAC/B,aAAa,EAAE,wBAAwB;QACvC,WAAW,EAAE,wBAAwB;QACrC,SAAS,EAAE,iBAAiB;QAC5B,KAAK,EAAE,kBAAkB;QACzB,aAAa,EAAE,wBAAwB;QACvC,WAAW,EAAE,4BAA4B;QACzC,YAAY,EAAE,eAAe;QAC7B,kBAAkB,EAChB,gFAAgF;QAClF,WAAW,EAAE,mCAAmC;QAChD,iBAAiB,EACf,sEAAsE;QACxE,WAAW,EAAE,4BAA4B;QACzC,iBAAiB,EACf,yFAAyF;QAC3F,MAAM,EAAE,mBAAmB;QAC3B,YAAY,EACV,yFAAyF;KAC5F;CACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openbrf/display",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "The Open BRF screen content contract, the rotation builder, the renderer and the fetch loop that every screen showing an association's content is built from.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"openbrf",
|
|
7
|
+
"brf",
|
|
8
|
+
"housing-cooperative",
|
|
9
|
+
"digital-signage",
|
|
10
|
+
"screen",
|
|
11
|
+
"renderer"
|
|
12
|
+
],
|
|
13
|
+
"license": "AGPL-3.0-only",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@openbrf/tokens": "^1.0.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^26.6.5",
|
|
27
|
+
"jsdom": "^30.1.3",
|
|
28
|
+
"typescript": "^7.0.2",
|
|
29
|
+
"vite-plus": "1.3.0"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"src",
|
|
36
|
+
"!src/**/*.spec.ts",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"LICENSE-EXCEPTION.md"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true,
|
|
43
|
+
"registry": "https://registry.npmjs.org/"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/openbrf/openbrf/tree/main/packages/display#readme",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/openbrf/openbrf.git",
|
|
49
|
+
"directory": "packages/display"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/openbrf/openbrf/issues"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc -p tsconfig.json",
|
|
56
|
+
"lint": "vp lint .",
|
|
57
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
58
|
+
"test": "vp test run",
|
|
59
|
+
"words": "node scripts/words.mjs"
|
|
60
|
+
}
|
|
6
61
|
}
|
package/src/contract.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The screen content contract, version 1: the document a screen fetches and
|
|
3
|
+
* shows, as ADR 0034 defined it and ADR 0050 made the core's.
|
|
4
|
+
*
|
|
5
|
+
* The text of record is docs/screen-content-contract.md. These types and
|
|
6
|
+
* limits are that text in code, and a change to either is a change to the
|
|
7
|
+
* contract under its version rules: a field or a body kind may be added within
|
|
8
|
+
* version 1, because an older player ignores what it does not know, and a
|
|
9
|
+
* change an older player would misread raises the version.
|
|
10
|
+
*
|
|
11
|
+
* "Characters" in a limit are Unicode code points. A server that measures in
|
|
12
|
+
* UTF-16 code units, as JavaScript's `length` does, never exceeds a limit
|
|
13
|
+
* measured in code points, so the builder in this package measures in code
|
|
14
|
+
* units and the parser in code points.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The version of the document this package reads and builds. */
|
|
18
|
+
export const SCREEN_CONTENT_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
/** An instant: RFC 3339 in UTC, whole seconds, with "Z": "2026-12-01T09:00:00Z". */
|
|
21
|
+
export type Instant = string;
|
|
22
|
+
|
|
23
|
+
/** A time of day, "HH:MM" from "00:00" to "24:00", in the document's `timeZone`. */
|
|
24
|
+
export type LocalTime = string;
|
|
25
|
+
|
|
26
|
+
/** The languages a screen speaks. */
|
|
27
|
+
export type ScreenLocale = "sv" | "en";
|
|
28
|
+
|
|
29
|
+
/** An ISO weekday: 1 is Monday and 7 is Sunday. */
|
|
30
|
+
export type IsoWeekday = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
31
|
+
|
|
32
|
+
/** How the screen lays out its canvas. */
|
|
33
|
+
export interface ScreenLayout {
|
|
34
|
+
/** Degrees clockwise the player turns its canvas. */
|
|
35
|
+
orientation: 0 | 90 | 180 | 270;
|
|
36
|
+
/** A band above the slides with the date and the time, or none. */
|
|
37
|
+
header: "none" | "clock";
|
|
38
|
+
theme: "light" | "dark";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One window in which the screen shows the rotation. */
|
|
42
|
+
export interface ScreenScheduleWindow {
|
|
43
|
+
days: IsoWeekday[];
|
|
44
|
+
/** Inclusive. */
|
|
45
|
+
from: LocalTime;
|
|
46
|
+
/** Exclusive, and after `from`. A window over midnight is two windows. */
|
|
47
|
+
to: LocalTime;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* When the screen shows the rotation. Outside every window it shows black;
|
|
52
|
+
* with no window at all it shows the rotation always.
|
|
53
|
+
*/
|
|
54
|
+
export interface ScreenSchedule {
|
|
55
|
+
/** At most 14. */
|
|
56
|
+
on: ScreenScheduleWindow[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One kind of thing in a screen's rotation (GLOSSARY.md, "source"). */
|
|
60
|
+
export type SlideSource =
|
|
61
|
+
"news" | "events" | "meeting" | "availability" | "picture" | "addon";
|
|
62
|
+
|
|
63
|
+
/** A picture the screen may show, fetched by its `mediaFileId`. */
|
|
64
|
+
export interface PictureRef {
|
|
65
|
+
mediaFileId: string;
|
|
66
|
+
/** The image type the core identified from the bytes. */
|
|
67
|
+
contentType: string;
|
|
68
|
+
byteSize: number;
|
|
69
|
+
/** Opaque, and changes when the file does: the picture's ETag. */
|
|
70
|
+
version: string;
|
|
71
|
+
/** At most 300 characters. */
|
|
72
|
+
alt: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A published news item. */
|
|
76
|
+
export interface NewsBody {
|
|
77
|
+
kind: "news";
|
|
78
|
+
/** At most 200 characters. */
|
|
79
|
+
title: string;
|
|
80
|
+
/** At most 2 000 characters together. */
|
|
81
|
+
paragraphs: string[];
|
|
82
|
+
picture: PictureRef | null;
|
|
83
|
+
publishedAt: Instant;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One occurrence of a published event, as an events slide lists it. */
|
|
87
|
+
export interface EventsOccurrence {
|
|
88
|
+
/** At most 200 characters. */
|
|
89
|
+
title: string;
|
|
90
|
+
/** At most 500 characters. */
|
|
91
|
+
description: string | null;
|
|
92
|
+
/** At most 200 characters. */
|
|
93
|
+
location: string | null;
|
|
94
|
+
startsAt: Instant;
|
|
95
|
+
endsAt: Instant;
|
|
96
|
+
calledOff: boolean;
|
|
97
|
+
placesLeft: number | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The coming occurrences of published events. */
|
|
101
|
+
export interface EventsBody {
|
|
102
|
+
kind: "events";
|
|
103
|
+
/** 1 to 8, soonest first. */
|
|
104
|
+
occurrences: EventsOccurrence[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** One matter on a general meeting's agenda. */
|
|
108
|
+
export interface AgendaItem {
|
|
109
|
+
position: number;
|
|
110
|
+
/** At most 200 characters. */
|
|
111
|
+
title: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The notice of a general meeting the board has called. */
|
|
115
|
+
export interface MeetingBody {
|
|
116
|
+
kind: "meeting";
|
|
117
|
+
meetingKind: "ordinary" | "extraordinary";
|
|
118
|
+
startsAt: Instant;
|
|
119
|
+
/** At most 200 characters. */
|
|
120
|
+
place: string;
|
|
121
|
+
/** At most 500 characters. */
|
|
122
|
+
digitalParticipation: string | null;
|
|
123
|
+
/** At most 40. */
|
|
124
|
+
agenda: AgendaItem[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A period a bookable resource is taken. Never by whom. */
|
|
128
|
+
export interface BusyInterval {
|
|
129
|
+
start: Instant;
|
|
130
|
+
end: Instant;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One bookable resource on an availability slide. */
|
|
134
|
+
export interface AvailabilityResource {
|
|
135
|
+
/** At most 80 characters. */
|
|
136
|
+
name: string;
|
|
137
|
+
mode: "timeSlots" | "wholeDay" | "dateRange";
|
|
138
|
+
slotMinutes: number | null;
|
|
139
|
+
/** Minutes after midnight in the document's `timeZone`. */
|
|
140
|
+
opensAtMinute: number | null;
|
|
141
|
+
closesAtMinute: number | null;
|
|
142
|
+
/** Merged, sorted, and inside [from, to). */
|
|
143
|
+
busy: BusyInterval[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** When the house's bookable resources are free and busy. */
|
|
147
|
+
export interface AvailabilityBody {
|
|
148
|
+
kind: "availability";
|
|
149
|
+
/** The fetch, to the minute; never earlier. */
|
|
150
|
+
from: Instant;
|
|
151
|
+
/** At most 7 days after `from`. */
|
|
152
|
+
to: Instant;
|
|
153
|
+
/** 1 to 12. */
|
|
154
|
+
resources: AvailabilityResource[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A picture the board chose for the screen. */
|
|
158
|
+
export interface PictureBody {
|
|
159
|
+
kind: "picture";
|
|
160
|
+
picture: PictureRef;
|
|
161
|
+
/** At most 200 characters. */
|
|
162
|
+
caption: string | null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** An addon's text slide. */
|
|
166
|
+
export interface AddonTextBody {
|
|
167
|
+
kind: "text";
|
|
168
|
+
heading: string;
|
|
169
|
+
/** At most 2 000 characters together. */
|
|
170
|
+
body: string[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** An addon's table slide. */
|
|
174
|
+
export interface AddonTableBody {
|
|
175
|
+
kind: "table";
|
|
176
|
+
heading: string;
|
|
177
|
+
/** At most 4. */
|
|
178
|
+
columns: string[];
|
|
179
|
+
/** At most 60, each as long as `columns`. */
|
|
180
|
+
rows: string[][];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** One door on an addon's doors slide. */
|
|
184
|
+
export interface AddonDoor {
|
|
185
|
+
floor: number | null;
|
|
186
|
+
apartment: string;
|
|
187
|
+
names: string[];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** An addon's doors slide: the names on the doors of one entrance. */
|
|
191
|
+
export interface AddonDoorsBody {
|
|
192
|
+
kind: "doors";
|
|
193
|
+
heading: string;
|
|
194
|
+
/** At most 120. */
|
|
195
|
+
doors: AddonDoor[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** One slide of an addon's output at the `source` extension point. */
|
|
199
|
+
export type AddonBody = AddonTextBody | AddonTableBody | AddonDoorsBody;
|
|
200
|
+
|
|
201
|
+
/** What a slide shows. Its `kind` matches the slide's `source`. */
|
|
202
|
+
export type SlideBody =
|
|
203
|
+
| NewsBody
|
|
204
|
+
| EventsBody
|
|
205
|
+
| MeetingBody
|
|
206
|
+
| AvailabilityBody
|
|
207
|
+
| PictureBody
|
|
208
|
+
| AddonBody;
|
|
209
|
+
|
|
210
|
+
/** One slide of the rotation. */
|
|
211
|
+
export interface Slide {
|
|
212
|
+
/** 1 to 64 of A-Z a-z 0-9 . _ : -, the same on every fetch while the slide shows the same thing. */
|
|
213
|
+
id: string;
|
|
214
|
+
/** Integer, 5 to 120: how long it stays. */
|
|
215
|
+
seconds: number;
|
|
216
|
+
/** The player skips the slide from this instant, without waiting for a fetch. */
|
|
217
|
+
until: Instant | null;
|
|
218
|
+
source: SlideSource;
|
|
219
|
+
/** The addon's plugin id when `source` is "addon", otherwise null. */
|
|
220
|
+
addon: string | null;
|
|
221
|
+
body: SlideBody;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The screen content document, version 1. */
|
|
225
|
+
export interface ScreenContent {
|
|
226
|
+
version: 1;
|
|
227
|
+
/** The device token's subject: the screen's id. */
|
|
228
|
+
screenId: string;
|
|
229
|
+
generatedAt: Instant;
|
|
230
|
+
/** Integer, 30 to 60. */
|
|
231
|
+
refreshSeconds: number;
|
|
232
|
+
locale: ScreenLocale;
|
|
233
|
+
/** IANA zone of the association's calendar day: "Europe/Stockholm". */
|
|
234
|
+
timeZone: string;
|
|
235
|
+
/** The only channel a paired screen's shell takes player releases from. */
|
|
236
|
+
releaseChannel: "stable" | "beta";
|
|
237
|
+
layout: ScreenLayout;
|
|
238
|
+
schedule: ScreenSchedule;
|
|
239
|
+
/** In order, at most 100; the player loops through them. */
|
|
240
|
+
rotation: Slide[];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The contract's limits, by what they bound. */
|
|
244
|
+
export const SCREEN_CONTENT_LIMITS = {
|
|
245
|
+
/** The document's size, in bytes of its UTF-8 JSON. */
|
|
246
|
+
documentBytes: 1024 * 1024,
|
|
247
|
+
rotation: 100,
|
|
248
|
+
slideSecondsMin: 5,
|
|
249
|
+
slideSecondsMax: 120,
|
|
250
|
+
refreshSecondsMin: 30,
|
|
251
|
+
refreshSecondsMax: 60,
|
|
252
|
+
scheduleWindows: 14,
|
|
253
|
+
title: 200,
|
|
254
|
+
/** All of a news item's paragraphs together. */
|
|
255
|
+
paragraphs: 2000,
|
|
256
|
+
description: 500,
|
|
257
|
+
location: 200,
|
|
258
|
+
place: 200,
|
|
259
|
+
digitalParticipation: 500,
|
|
260
|
+
agendaItems: 40,
|
|
261
|
+
agendaTitle: 200,
|
|
262
|
+
occurrencesMin: 1,
|
|
263
|
+
occurrencesMax: 8,
|
|
264
|
+
resourcesMin: 1,
|
|
265
|
+
resourcesMax: 12,
|
|
266
|
+
resourceName: 80,
|
|
267
|
+
/** How far an availability slide's `to` may be after its `from`, in days. */
|
|
268
|
+
availabilityDays: 7,
|
|
269
|
+
alt: 300,
|
|
270
|
+
caption: 200,
|
|
271
|
+
/** All of an addon text slide's paragraphs together. */
|
|
272
|
+
addonTextBody: 2000,
|
|
273
|
+
tableColumns: 4,
|
|
274
|
+
tableRows: 60,
|
|
275
|
+
doors: 120,
|
|
276
|
+
} as const;
|
|
277
|
+
|
|
278
|
+
/** What a slide's `id` is made of. */
|
|
279
|
+
export const SLIDE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
|