From 24ef529d48de0bb592e46a88671970989dafe329 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Mon, 19 Feb 2024 13:58:32 +0100 Subject: [PATCH 01/16] restructured the files --- .vscode/launch.json | 4 +- .vscode/tasks.json | 9 ++++ README.md | 6 ++- casparcg-template/{ => JohnCG}/.eslintrc | 0 .../{song.html => JohnCG/Song.html} | 0 client/main.css | 6 ++- .../src => src/server}/JGCPReceiveMessages.ts | 0 .../src => src/server}/JGCPSendMessages.ts | 2 +- .../src/sequence.ts => src/server/Sequence.ts | 52 +++++++++---------- {server/src => src/server}/SongFile.ts | 0 {server/src => src/server}/config.ts | 0 {server/src => src/server}/control.ts | 0 {server/src => src/server}/http-server.ts | 0 {server/src => src/server}/main.ts | 0 {server/src => src/server}/osc-server.ts | 0 .../src => src/server}/websocket-server.ts | 0 tsconfig.json | 4 +- 17 files changed, 48 insertions(+), 35 deletions(-) rename casparcg-template/{ => JohnCG}/.eslintrc (100%) rename casparcg-template/{song.html => JohnCG/Song.html} (100%) rename {server/src => src/server}/JGCPReceiveMessages.ts (100%) rename {server/src => src/server}/JGCPSendMessages.ts (93%) rename server/src/sequence.ts => src/server/Sequence.ts (96%) rename {server/src => src/server}/SongFile.ts (100%) rename {server/src => src/server}/config.ts (100%) rename {server/src => src/server}/control.ts (100%) rename {server/src => src/server}/http-server.ts (100%) rename {server/src => src/server}/main.ts (100%) rename {server/src => src/server}/osc-server.ts (100%) rename {server/src => src/server}/websocket-server.ts (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 0b4663e..bb5d8de 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,8 +11,8 @@ "skipFiles": [ "/**" ], - "program": "${workspaceFolder}\\server\\src\\main.ts", - "preLaunchTask": "tsc: build - tsconfig.json", + "program": "${workspaceFolder}\\src\\server\\main.ts", + "preLaunchTask": "tsc: build - server", "outFiles": [ "${workspaceFolder}/out/**/*.js" ], diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 90a6cd3..143fecf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -11,6 +11,15 @@ "problemMatcher": [], "label": "npm: build", "detail": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./build.ps1" + }, + { + "type": "typescript", + "tsconfig": "tsconfig.json", + "problemMatcher": [ + "$tsc" + ], + "group": "build", + "label": "tsc: build - server" } ] } \ No newline at end of file diff --git a/README.md b/README.md index 577d6c1..b607e55 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,9 @@ Generate lyric-graphics and play them out through CasparCG. - create message log / show error messages - write installation instruction - client communication with osc over websocket? -- client: don't use ifames, include html directly / alternatively: clone iframes instead of creating new ones +- client: don't use iframes, include html directly / alternatively: clone iframes instead of creating new ones - add support for NodeCG - look into document fragments -- add CLI output to server \ No newline at end of file +- add CLI output to server +- CasparCG: split text and image in two layers: enables text without background +- rewrite client in typescript \ No newline at end of file diff --git a/casparcg-template/.eslintrc b/casparcg-template/JohnCG/.eslintrc similarity index 100% rename from casparcg-template/.eslintrc rename to casparcg-template/JohnCG/.eslintrc diff --git a/casparcg-template/song.html b/casparcg-template/JohnCG/Song.html similarity index 100% rename from casparcg-template/song.html rename to casparcg-template/JohnCG/Song.html diff --git a/client/main.css b/client/main.css index 472c86e..a9becd8 100644 --- a/client/main.css +++ b/client/main.css @@ -126,7 +126,9 @@ div.button, div.button > * { } #sequence { - width: 32rem; + width: 24rem; + + resize: horizontal; } .header { @@ -183,6 +185,8 @@ div.button, div.button > * { padding-left: 0.5rem; flex: 1; + + text-wrap: nowrap; } .sequence_item_container:hover > .sequence_item { diff --git a/server/src/JGCPReceiveMessages.ts b/src/server/JGCPReceiveMessages.ts similarity index 100% rename from server/src/JGCPReceiveMessages.ts rename to src/server/JGCPReceiveMessages.ts diff --git a/server/src/JGCPSendMessages.ts b/src/server/JGCPSendMessages.ts similarity index 93% rename from server/src/JGCPSendMessages.ts rename to src/server/JGCPSendMessages.ts index 0def53f..b327c81 100644 --- a/server/src/JGCPSendMessages.ts +++ b/src/server/JGCPSendMessages.ts @@ -1,4 +1,4 @@ -import * as SequenceClass from "./Sequence"; +import * as SequenceClass from "../server/Sequence"; /** * Base interface for sent JGCP-messages diff --git a/server/src/sequence.ts b/src/server/Sequence.ts similarity index 96% rename from server/src/sequence.ts rename to src/server/Sequence.ts index 6b98449..59b61cd 100644 --- a/server/src/sequence.ts +++ b/src/server/Sequence.ts @@ -27,14 +27,6 @@ interface SequenceItem extends SequenceItemPartial { Color: string; } -// interface SequenceItemSong extends SequenceItem { -// Type: "Song"; -// Song: SongFile; -// Language: number[]; -// VerseOrder: string[]; -// FileName: string; -// } - // interface for a renderer-object interface RenderObject { slides: ItemSlide[]; @@ -101,15 +93,24 @@ class Sequence { this.parse_sequence(sequence); // create the casparcg-connections - // use for loop to maintain the order - for (const connection_setting of Config.casparcg.connections) { - this.casparcg_connections.push( - new CasparCG({ - ...connection_setting, - autoConnect: true - }) - ); - } + Config.casparcg.connections.forEach((connection_setting, index) => { + const casparcg_connection = new CasparCG({ + ...connection_setting, + autoConnect: true + }); + + // add a listener to send send the current-slide on connection + casparcg_connection.addListener("connect", () => { + // load the active-item + this.casparcg_load_item(this.active_item, this.active_slide, index, false); + + // remove the connect-listener again + casparcg_connection.removeAllListeners("connect"); + }); + + // add the connection to the stored connections + this.casparcg_connections.push(casparcg_connection); + }); // clear the previous casparcg-output on the layers this.casparcg_clear_layers(); @@ -132,16 +133,13 @@ class Sequence { casparcg_clear_layers() { // setup auto-loading of the current-item on reconnection this.casparcg_connections.forEach((casparcg_connection, index) => { - casparcg_connection.addListener("disconnect", () => { - // on disconnect register a connect-listener - casparcg_connection.addListener("connect", () => { - // load the active-item - this.casparcg_load_item(this.active_item, this.active_slide, index, false); - - // remove the connect-listener again - casparcg_connection.removeAllListeners("connect"); - }); - + casparcg_connection.cgClear({ + channel: Config.casparcg.connections[index].channel, + layer: Config.casparcg.connections[index].layers[0] + }); + casparcg_connection.cgClear({ + channel: Config.casparcg.connections[index].channel, + layer: Config.casparcg.connections[index].layers[1] }); }); } diff --git a/server/src/SongFile.ts b/src/server/SongFile.ts similarity index 100% rename from server/src/SongFile.ts rename to src/server/SongFile.ts diff --git a/server/src/config.ts b/src/server/config.ts similarity index 100% rename from server/src/config.ts rename to src/server/config.ts diff --git a/server/src/control.ts b/src/server/control.ts similarity index 100% rename from server/src/control.ts rename to src/server/control.ts diff --git a/server/src/http-server.ts b/src/server/http-server.ts similarity index 100% rename from server/src/http-server.ts rename to src/server/http-server.ts diff --git a/server/src/main.ts b/src/server/main.ts similarity index 100% rename from server/src/main.ts rename to src/server/main.ts diff --git a/server/src/osc-server.ts b/src/server/osc-server.ts similarity index 100% rename from server/src/osc-server.ts rename to src/server/osc-server.ts diff --git a/server/src/websocket-server.ts b/src/server/websocket-server.ts similarity index 100% rename from server/src/websocket-server.ts rename to src/server/websocket-server.ts diff --git a/tsconfig.json b/tsconfig.json index 9522938..cf4ff5c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "noUnusedLocals": true, "removeComments": true }, - "exclude": [ - "node_modules" + "include": [ + "src\\server" ] } \ No newline at end of file From 896b26e9f21b75baa41d29db0f8fc2982bf12aa4 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Tue, 20 Feb 2024 00:33:17 +0100 Subject: [PATCH 02/16] Restructured sequence.ts to prepare for more sequence-items --- README.md | 3 +- build.ps1 | 6 +- casparcg-template/JohnCG/Song.html | 367 ----------------- .../JohnCG/.eslintrc | 0 casparcg-templates/JohnCG/Song.html | 372 +++++++++++++++++ client/main.js | 12 +- src/server/JGCPSendMessages.ts | 3 +- src/server/Sequence.ts | 380 ++++-------------- src/server/SequenceItems/Countdown.ts | 0 src/server/SequenceItems/SequenceItem.ts | 58 +++ src/server/SequenceItems/Song.ts | 240 +++++++++++ src/server/{ => SequenceItems}/SongFile.ts | 2 +- src/server/control.ts | 40 +- src/server/main.ts | 2 +- src/server/{ => servers}/http-server.ts | 2 +- src/server/{ => servers}/osc-server.ts | 0 src/server/{ => servers}/websocket-server.ts | 0 tsconfig.json | 1 - 18 files changed, 773 insertions(+), 715 deletions(-) delete mode 100644 casparcg-template/JohnCG/Song.html rename {casparcg-template => casparcg-templates}/JohnCG/.eslintrc (100%) create mode 100644 casparcg-templates/JohnCG/Song.html create mode 100644 src/server/SequenceItems/Countdown.ts create mode 100644 src/server/SequenceItems/SequenceItem.ts create mode 100644 src/server/SequenceItems/Song.ts rename src/server/{ => SequenceItems}/SongFile.ts (99%) rename src/server/{ => servers}/http-server.ts (97%) rename src/server/{ => servers}/osc-server.ts (100%) rename src/server/{ => servers}/websocket-server.ts (100%) diff --git a/README.md b/README.md index b607e55..7703d14 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,5 @@ Generate lyric-graphics and play them out through CasparCG. - look into document fragments - add CLI output to server - CasparCG: split text and image in two layers: enables text without background -- rewrite client in typescript \ No newline at end of file +- rewrite client in typescript +- create dummy-sequence-items for unsupported ones \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 17dfa94..87ece63 100644 --- a/build.ps1 +++ b/build.ps1 @@ -8,7 +8,7 @@ New-Item -Type Directory .\Dist\build New-Item -Type Directory .\Dist\$build_name # bundle the files -yarn esbuild server/src/main.ts --bundle --platform=node --outfile=dist/build/main.js +yarn esbuild src/server/main.ts --bundle --platform=node --outfile=dist/build/main.js # create sea-prep.blob node --experimental-sea-config .\sea-config.json @@ -23,7 +23,9 @@ node -e "require('fs').copyFileSync(process.execPath, 'dist/build/$build_name.ex yarn postject dist/build/$build_name.exe NODE_SEA_BLOB dist/build/sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 # copy the files in the output -Copy-Item -Path .\config.json,.\casparcg-template,.\client -Destination .\dist\$build_name -Exclude .eslintrc -Recurse +Copy-Item -Path .\config.json -Destination .\dist\$build_name -Exclude .eslintrc +Copy-Item -Path .\casparcg-templates -Destination .\dist\$build_name -Exclude .eslintrc -Recurse +Copy-Item -Path .\client -Destination .\dist\$build_name -Exclude .eslintrc -Recurse Copy-Item -Path .\dist\build\$build_name.exe .\dist\$build_name -Recurse # pack the files in a .tar.gz-file diff --git a/casparcg-template/JohnCG/Song.html b/casparcg-template/JohnCG/Song.html deleted file mode 100644 index 117cafa..0000000 --- a/casparcg-template/JohnCG/Song.html +++ /dev/null @@ -1,367 +0,0 @@ - - - - - - - johnCG - template - - - - -
-
- -
-
-
- - - \ No newline at end of file diff --git a/casparcg-template/JohnCG/.eslintrc b/casparcg-templates/JohnCG/.eslintrc similarity index 100% rename from casparcg-template/JohnCG/.eslintrc rename to casparcg-templates/JohnCG/.eslintrc diff --git a/casparcg-templates/JohnCG/Song.html b/casparcg-templates/JohnCG/Song.html new file mode 100644 index 0000000..9ef7b3c --- /dev/null +++ b/casparcg-templates/JohnCG/Song.html @@ -0,0 +1,372 @@ + + + + + + + johnCG - template + + + + +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/client/main.js b/client/main.js index a0498e9..907c63e 100644 --- a/client/main.js +++ b/client/main.js @@ -53,17 +53,17 @@ function display_items(data) { for (let item of data.sequence_items) { const div_sequence_item_container = document.createElement("div"); div_sequence_item_container.classList.add("sequence_item_container"); - div_sequence_item_container.dataset.item_number = item.item; + div_sequence_item_container.dataset.item_number = item.Item; const div_sequence_item_color_indicator = document.createElement("div"); div_sequence_item_color_indicator.classList.add("item_color_indicator"); - div_sequence_item_color_indicator.style.backgroundColor = item.color; + div_sequence_item_color_indicator.style.backgroundColor = item.Color; div_sequence_item_container.append(div_sequence_item_color_indicator); const div_sequence_item = document.createElement("div"); div_sequence_item.classList.add("sequence_item"); - div_sequence_item.innerText = item.caption; + div_sequence_item.innerText = item.Caption; div_sequence_item_container.onclick = function() { request_item_slides(Number(this.dataset.item_number)); @@ -74,7 +74,7 @@ function display_items(data) { } // display the visibility state - display_visibility_state(data.metadata.visibility); + display_visibility_state(data.visibility); } function request_item_slides(item) { @@ -99,7 +99,7 @@ function display_item_slides(data) { const div_slides_view_container = document.querySelector("#slides_view_container"); // select the sequence-item - select_item(data.metadata.item); + select_item(data.item); let slide_counter = 0; @@ -129,7 +129,7 @@ function display_item_slides(data) { switch (part.type) { case "title": { - div_slide_part_header.innerText = data.metadata.title; + div_slide_part_header.innerText = data.title; } break; case "lyric": { div_slide_part_header.innerText = part.part; diff --git a/src/server/JGCPSendMessages.ts b/src/server/JGCPSendMessages.ts index b327c81..1bc399c 100644 --- a/src/server/JGCPSendMessages.ts +++ b/src/server/JGCPSendMessages.ts @@ -1,4 +1,5 @@ import * as SequenceClass from "../server/Sequence"; +import { ClientItemSlides } from "./SequenceItems/SequenceItem"; /** * Base interface for sent JGCP-messages @@ -32,7 +33,7 @@ export interface State extends Base { visibility?: boolean; } -export interface ItemSlides extends Base, SequenceClass.ClientItemSlides { +export interface ItemSlides extends Base, ClientItemSlides { clientID: string; command: "item-slides"; } diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index 59b61cd..f0a02b0 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -1,89 +1,32 @@ import path from "path"; import { CasparCG } from "casparcg-connection"; -import fs from "fs"; -import mime from "mime-types"; -import SongFile, { ItemPartClient, LyricPart, SongElement, TitlePart } from "./SongFile"; +import { SongElement } from "./SequenceItems/SongFile"; +import SequenceItem, { ClientItemSlides, ItemProps } from "./SequenceItems/SequenceItem"; +import { Song, SongProps } from "./SequenceItems/Song"; -import Config from "./config"; - -// individual data of the sequence-file -interface SequenceItemPartial { - Caption?: string; - Color?: string; - Type?: string; - FileName?: string; - VerseOrder?: string[]; - Song?: SongFile; - Language?: number; - PrimaryLanguage?: number; - Languages?: number[]; -} - -// individual data of the sequence-file with Caption mandatory -interface SequenceItem extends SequenceItemPartial { - Caption: string; - SlideCount: number; - Color: string; -} - -// interface for a renderer-object -interface RenderObject { - slides: ItemSlide[]; - slide: number; - languages: number[]; - backgroundImage?: string; -} +import * as JGCPSend from "./JGCPSendMessages"; -interface ClientSequenceItem { - caption: string; - item: number; - color; -} +import Config from "./config"; interface ClientSequenceItems { - sequence_items: ClientSequenceItem[]; + sequence_items: ItemProps[]; metadata: { item: number; - slide: number; visibility: boolean; } } -interface ClientItemSlides { - metadata: { - title: string; - item: number; - }; - slides: ItemPartClient[]; - slides_template: RenderObject; -} - interface ActiveItemSlide { item: number, slide: number } -interface TitleSlide extends TitlePart { -} - -interface LyricSlide { - type: "lyric"; - data: string[][]; -} - -type ItemSlide = LyricSlide | TitleSlide; - -const _item_navigate_type = ["item", "slide"] as const; -type NavigateType = (typeof _item_navigate_type)[number]; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isItemNavigateType = (x: any): x is NavigateType => _item_navigate_type.includes(x); - class Sequence { // store the individual items of the sequence sequence_items: SequenceItem[] = []; - private active: ActiveItemSlide = { item: 0, slide: 0 }; + private active_item_number: number = 0; private casparcg_visibility: boolean = Config.behaviour.showOnLoad; @@ -102,7 +45,7 @@ class Sequence { // add a listener to send send the current-slide on connection casparcg_connection.addListener("connect", () => { // load the active-item - this.casparcg_load_item(this.active_item, this.active_slide, index, false); + this.casparcg_load_item(this.active_item_number, index, false); // remove the connect-listener again casparcg_connection.removeAllListeners("connect"); @@ -170,7 +113,7 @@ class Sequence { } // process every element individually - for (const sequence_item of re_results) { + re_results.forEach((sequence_item) => { // reset the regex re_scan_sequence_item.lastIndex = 0; @@ -178,10 +121,13 @@ class Sequence { let re_results_item: RegExpExecArray; // store the data of the object - let item_data: SequenceItem = { + let item_data: ItemProps = { Caption: "", Color: "", - SlideCount: 0 + SlideCount: 0, + Type: null, + Item: this.sequence_items.length, + FileName: null }; // exec the item-regex until there are no more results @@ -195,7 +141,7 @@ class Sequence { // remove all undefined values Object.keys(results).forEach(key => results[key] === undefined && delete results[key]); - // merge the result with the data object + // parse all remaining values item_data = { ...item_data, ...parse_item_value_string(...Object.entries(results)[0]) }; } @@ -205,191 +151,46 @@ class Sequence { // TESTING only if it is song-element, since the others aren't implemented if (item_data.Type === "Song") { - try { - item_data.Song = new SongFile(get_song_path(item_data.FileName)); - } catch (e) { - // if the error is because the file doesn't exist, skip the rest of the loop iteration - if (e.code === "ENOENT") { - console.debug(`song '${item_data.FileName}' does not exist`); - continue; - } else { - throw e; - } - } - - // create the languages-array - // initialize the array with all languages - item_data.Languages = Array.from(Array(item_data.Song.languages).keys()); - - // if there is a 'PrimaryLanguage' specified, move it to the first position - if (item_data.PrimaryLanguage !== undefined) { - const temp = item_data.Languages.splice(item_data.PrimaryLanguage, 1); - item_data.Languages.unshift(...temp); - } - // if a 'Language' is specified, take only this one - if (item_data.Language !== undefined) { - item_data.Languages = [item_data.Languages[item_data.Language]]; - } - - // add the title-slide to the counter - item_data.SlideCount++; - - // count the slides - for (const part of this.get_verse_order(item_data)) { - // check wether the part is actually defined in the songfile - try { - item_data.SlideCount += item_data.Song.get_part(part).slides.length; - } catch (e) { - if (!(e instanceof ReferenceError)) { - throw e; - } - } - } - - this.sequence_items.push(item_data); + this.sequence_items.push(new Song(item_data as SongProps)); } - } + }); } - // package an item into a package for the casparcg-template renderer - private create_renderer_object(item: number, slide: number): RenderObject { - item = this.validate_item_number(item); - slide = this.validate_slide_number(slide, item); - - const sequence_item = this.sequence_items[item]; - - const return_object: RenderObject = { - slides: [ - sequence_item.Song.title - ], - languages: sequence_item.Languages, - slide: slide - }; - - // add the individual parts to the output-object - for (const part_name of this.get_verse_order(item)) { - let part: LyricPart = undefined; - try { - part = sequence_item.Song.get_part(part_name); - } catch (e) { - if (!(e instanceof ReferenceError)) { - throw e; - } - } - - // if a part is not available, skip it - if (part !== undefined){ - // add the individual slides of the part to the output object - for (const slide of part.slides) { - return_object.slides.push({ - type: part.type, - data: slide - }); - } - } - } - - return_object.backgroundImage = get_image_b64(sequence_item.Song.metadata.BackgroundImage); - - return return_object; - } - create_client_object_sequence(): ClientSequenceItems { const return_sequence: ClientSequenceItems = { - sequence_items: [], + sequence_items: this.sequence_items.map((item) => item.props), metadata: { - item: this.active_item, - slide: this.active_slide, + item: this.active_item_number, + // slide: this.active_slide, visibility: this.visibility } }; - for (const [i, song_item] of this.sequence_items.entries()) { - const current_item: ClientSequenceItem = { - caption: song_item.Caption, - color: song_item.Color, - item: i - }; - - return_sequence.sequence_items.push(current_item); - } - return return_sequence; } - create_client_object_item_slides(item: number): ClientItemSlides{ - item = this.validate_item_number(item); - - const current_item: SequenceItem = this.sequence_items[item]; - - const return_item: ClientItemSlides = { - metadata: { - title: current_item.Caption, - item - }, - slides: [ - current_item.Song.get_title_client() - ], - slides_template: this.create_renderer_object(item, 0) - }; - - for (const part_name of this.get_verse_order(item)) { - let part = undefined; - - try { - part = current_item.Song.get_part_client(part_name); - } catch (e) { - if (!(e instanceof ReferenceError)) { - throw e; - } - } - - // if a part is not available, skip it - if (part !== undefined){ - return_item.slides.push(part); - } - } - - return return_item; - } - - get_verse_order(item: number | SequenceItem): string[] { - let sequence_item: SequenceItem; - - if (typeof item === "number") { - item = this.validate_item_number(item); - - sequence_item = this.sequence_items[item]; - } else { - sequence_item = item; - } - - if (sequence_item.VerseOrder !== undefined) { - return sequence_item.VerseOrder; - } else { - return sequence_item.Song.metadata.VerseOrder; - } + create_client_object_item_slides(item: number): ClientItemSlides { + return this.sequence_items[item].create_client_object_item_slides(); } set_active_item(item: number, slide: number = 0): ActiveItemSlide { item = this.validate_item_number(item); - slide = this.validate_slide_number(slide, item); - - this.active = { item, slide }; - this.casparcg_load_item(this.active.item, this.active.slide); + this.active_item_number = item; - return this.active; - } + this.active_sequence_item.set_active_slide(slide); - set_active_slide(slide): ActiveItemSlide { - slide = this.validate_slide_number(slide); + this.casparcg_load_item(this.active_item); - this.active.slide = slide; + return this.active_item_slide; + } + set_active_slide(slide): number { + const response = this.active_sequence_item.set_active_slide(slide); + this.casparcg_select_slide(this.active_slide); - - return this.active; + + return response; } /** @@ -406,7 +207,7 @@ class Sequence { throw new RangeError(`steps must be -1 or 1, but is ${steps}`); } - let new_active_item_number = this.active_item + steps; + let new_active_item_number = this.active_item_number + steps; // new active item has negative index -> roll over to other end if (new_active_item_number < 0) { @@ -425,36 +226,15 @@ class Sequence { * @returns wether the slide has been changed */ navigate_slide(steps: number): boolean { - if (typeof steps !== "number") { - throw new TypeError(`steps ('${steps}') is no number`); - } - - if (![-1, 1].includes(steps)) { - throw new RangeError(`steps must be -1 or 1, but is ${steps}`); - } - - let new_active_slide_number = this.active_slide + steps; - let slideChange = false; + const item_steps = this.active_sequence_item.navigate_slide(steps); - // new active item has negative index -> roll over to the last slide of the previous element - if (new_active_slide_number < 0) { - this.navigate_item(-1, -1); - - new_active_slide_number = this.sequence_items[this.active_item].SlideCount - 1; - // index is bigger than the slide-count -> roll over to zero - } else if (new_active_slide_number >= this.sequence_items[this.active_item].SlideCount) { - this.navigate_item(1); - - new_active_slide_number = 0; + if (item_steps !== 0) { + this.navigate_item(steps); } else { - slideChange = false; + this.casparcg_select_slide(this.active_sequence_item.active_slide); } - this.active.slide = new_active_slide_number; - - this.casparcg_select_slide(this.active_slide); - - return slideChange; + return item_steps !== 0; } private validate_item_number(item: number): number { @@ -471,25 +251,7 @@ class Sequence { return item; } - private validate_slide_number(slide: number, item?: number,): number { - if (item === undefined) { - item = this.active_item; - } - - const slide_count = this.sequence_items[item].SlideCount; - - if (slide < -slide_count || slide >= slide_count) { - throw new RangeError(`slide-number is out of range (${-slide_count}-${slide_count - 1})`); - } - - if (slide < 0) { - slide += slide_count; - } - - return slide; - } - - private casparcg_load_item(item: number, slide: number, index?: number, flip_layer: boolean = true): void { + private casparcg_load_item(item: number, index?: number, flip_layer: boolean = true): void { if (flip_layer) { // clear the lower layer this.casparcg_connections.forEach((casparcg_connection, loop_index) => { @@ -520,7 +282,7 @@ class Sequence { playOnLoad: this.casparcg_visibility, template: Config.casparcg.templates.song, // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug - data: JSON.stringify(JSON.stringify(this.create_renderer_object(item, slide), (key, val) => { + data: JSON.stringify(JSON.stringify(this.active_sequence_item.create_renderer_object(), (_key, val) => { if (typeof val === "string") { return val.replace("\"", "\\u0022"); } else { @@ -568,86 +330,85 @@ class Sequence { } get active_item(): number { - return this.active.item; + return this.active_item_number; + } + + get active_sequence_item(): SequenceItem { + return this.sequence_items[this.active_item_number]; } get active_slide(): number { - return this.active.slide; + return this.active_sequence_item.active_slide; } get active_item_slide(): ActiveItemSlide { return { - item: this.active_item, - slide: this.active_slide + item: this.active_item_number, + slide: this.active_sequence_item.active_slide }; } get visibility(): boolean { return this.casparcg_visibility; } + + get state(): JGCPSend.State { + return { + command: "state", + activeItemSlide: this.active_item_slide, + visibility: this.visibility + }; + } } // parse an individual sequence-item-value -function parse_item_value_string(key: string, value: string): SequenceItemPartial { +function parse_item_value_string(key: string, value: string): { [P in keyof ItemProps]?: ItemProps[P]; } { // remove line-breaks value = value.replaceAll(/'\s\+\s+'/gm, ""); // un-escape escaped characters value = value.replaceAll(/'#(\d+)'/gm, (_match, group) => String.fromCharCode(group)); - const result: SequenceItemPartial = { - }; - + const return_props: { [P in keyof ItemProps]?: ItemProps[P]; } = {}; + // do value-type specific stuff switch (key) { case "Data": // remove whitespace and linebreaks - result[key] = value.replaceAll(/\s+/gm, ""); + return_props[key] = value.replaceAll(/\s+/gm, ""); break; case "VerseOrder": // split csv-line into an array - result.VerseOrder = value.split(",") as SongElement[]; + return_props.VerseOrder = value.split(",") as SongElement[]; break; case "FileName": // assume the type from the file-extension if (path.extname(value) === ".sng") { - result.Type = "Song"; + return_props.Type = "Song"; } - result[key] = value; + return_props[key] = value; break; case "Color": if (value.substring(0, 2) === "cl") { - result[key] = convert_color_to_hex(value); + return_props[key] = convert_color_to_hex(value); } else { const color_int = Number(value); - const color = (color_int & 0x0000ff) >> 16 | (color_int & 0x00ff00) | (color_int & 0xff0000) >> 16; + const color = (color_int & 0x0000ff) << 16 | (color_int & 0x00ff00) | (color_int & 0xff0000) >> 16; const color_string = color.toString(16); - result[key] = "#" + color_string.padStart(6, "0"); + return_props[key] = "#" + color_string.padStart(6, "0"); } break; case "PrimaryLanguage": case "Language": - result[key] = Number(value) - 1; // subtract 1, because Songbeamer start counting at 1 + return_props[key] = Number(value) - 1; // subtract 1, because Songbeamer start counting at 1 break; default: - result[key] = value; + return_props[key] = value; break; } - return result; -} - -function get_image_b64(image_path: string): string { - try { - return `data:${mime.lookup(image_path)};base64,` + fs.readFileSync(path.join(Config.path.backgroundImage, image_path)).toString("base64"); - } catch (e) { - return ""; - } -} - -function get_song_path(song_path: string): string { - return path.join(Config.path.song, song_path); + return return_props; } function convert_color_to_hex(color: string): string | undefined { @@ -798,5 +559,6 @@ function convert_color_to_hex(color: string): string | undefined { return colours[color.toLowerCase()]; } -export { NavigateType, isItemNavigateType, ClientSequenceItems, ClientItemSlides, ActiveItemSlide }; +// export { NavigateType, isItemNavigateType, ClientSequenceItems, ClientItemSlides, ActiveItemSlide }; +export { ClientSequenceItems, ActiveItemSlide }; export default Sequence; diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts new file mode 100644 index 0000000..d88de50 --- /dev/null +++ b/src/server/SequenceItems/SequenceItem.ts @@ -0,0 +1,58 @@ +import fs from "fs"; +import path from "path"; +import mime from "mime-types"; + +import Config from "../config"; +import { SongProps } from "./Song"; + + +export interface ItemPropsBase { + Caption: string; + SlideCount: number; + Color: string; + Item: number; +} + +export type ItemProps = SongProps + +// interface for a renderer-object +export interface RenderObject { + slides: object; + slide: number; + backgroundImage?: string; +} + +export interface ClientItemSlides { + title: string; + item: number; + slides: object; + slides_template: RenderObject; +} + +export default abstract class SequenceItem { + protected abstract item_props: ItemProps; + + abstract create_renderer_object(slide?: number); + abstract create_client_object_item_slides(): ClientItemSlides; + abstract set_active_slide(slide?: number): number; + /** + * navigate the selected slide + * @param steps steps to navigate; sign is used for direction + * @returns required navigation of item; sign is used for direction + */ + abstract navigate_slide(steps: number): number; + + abstract get active_slide(): number; + + get props(): ItemProps { + return this.item_props; + } +} + +export function get_image_b64(image_path: string): string { + try { + return `data:${mime.lookup(image_path)};base64,` + fs.readFileSync(path.join(Config.path.backgroundImage, image_path)).toString("base64"); + } catch (e) { + return ""; + } +} \ No newline at end of file diff --git a/src/server/SequenceItems/Song.ts b/src/server/SequenceItems/Song.ts new file mode 100644 index 0000000..11e74e9 --- /dev/null +++ b/src/server/SequenceItems/Song.ts @@ -0,0 +1,240 @@ +import SequenceItem, { ClientItemSlides, ItemPropsBase, RenderObject, get_image_b64 } from "./SequenceItem"; +import SongFile, { ItemPartClient, LyricPart, TitlePart } from "./SongFile"; +import path from "path"; + +import Config from "../config"; + +export interface SongProps extends ItemPropsBase { + Type: "Song"; + FileName: string; + VerseOrder?: string[]; + Language?: number; + PrimaryLanguage?: number; +} + +interface TitleSlide extends TitlePart { +} + +interface LyricSlide { + type: "lyric"; + data: string[][]; +} + +type ItemSlide = LyricSlide | TitleSlide; + +interface SongRenderObject extends RenderObject { + type: "Song"; + slides: ItemSlide[]; + languages: number[]; +} + +export interface ClientSongItemSlides extends ClientItemSlides { + slides: ItemPartClient[]; + slides_template: RenderObject; +} + +export class Song extends SequenceItem { + protected item_props: SongProps; + + // amount of slides this element has + private SlideCount: number = 0; + // currently active slide-number + private active_slide_number: number = 0; + + private languages: number[]; + + private SongFile: SongFile; + + constructor(data: SongProps) { + super(); + + this.item_props = data; + + try { + this.SongFile = new SongFile(get_song_path(data.FileName)); + } catch (e) { + // if the error is because the file doesn't exist, skip the rest of the loop iteration + if (e.code === "ENOENT") { + console.debug(`song '${data.FileName}' does not exist`); + return null; + } else { + throw e; + } + } + + // create the languages-array + // initialize the array with all languages + this.languages = Array.from(Array(this.SongFile.languages).keys()); + + // if there is a 'PrimaryLanguage' specified, move it to the first position + if (this.item_props.PrimaryLanguage !== undefined) { + const temp = this.languages.splice(this.item_props.PrimaryLanguage, 1); + this.languages.unshift(...temp); + } + // if a 'Language' is specified, take only this one + if (this.item_props.Language !== undefined) { + this.languages = [this.languages[this.item_props.Language]]; + } + + // add the title-slide to the counter + this.SlideCount++; + + // count the slides + for (const part of this.get_verse_order()) { + // check wether the part is actually defined in the songfile + try { + this.SlideCount += this.SongFile.get_part(part).slides.length; + } catch (e) { + if (!(e instanceof ReferenceError)) { + throw e; + } + } + } + + } + + get_verse_order(): string[] { + if (this.item_props.VerseOrder !== undefined) { + return this.item_props.VerseOrder; + } else { + return this.SongFile.metadata.VerseOrder; + } + } + + create_renderer_object(slide: number): SongRenderObject { + if (slide === undefined) { + slide = this.active_slide; + } + + slide = this.validate_slide_number(slide); + + const return_object: SongRenderObject = { + type: "Song", + slides: [ + this.SongFile.part_title + ], + slide, + languages: this.languages + }; + + // add the individual parts to the output-object + for (const part_name of this.get_verse_order()) { + let part: LyricPart = undefined; + try { + part = this.SongFile.get_part(part_name); + } catch (e) { + if (!(e instanceof ReferenceError)) { + throw e; + } + } + + // if a part is not available, skip it + if (part !== undefined){ + // add the individual slides of the part to the output object + for (const slide of part.slides) { + return_object.slides.push({ + type: part.type, + data: slide + }); + } + } + } + + return_object.backgroundImage = get_image_b64(this.SongFile.metadata.BackgroundImage); + + return return_object; + } + + /** + * set the slide-number as active + * @param slide + */ + set_active_slide(slide: number): number { + slide = this.validate_slide_number(slide); + + this.active_slide_number = slide; + + return this.active_slide_number; + } + + navigate_slide(steps: number): number { + if (typeof steps !== "number") { + throw new TypeError(`steps ('${steps}') is no number`); + } + + if (![-1, 1].includes(steps)) { + throw new RangeError(`steps must be -1 or 1, but is ${steps}`); + } + + const new_active_slide_number = this.active_slide + steps; + let slide_steps = 0; + + // new active item has negative index -> roll over to the last slide of the previous element + if (new_active_slide_number < 0) { + slide_steps = -1; + + // index is bigger than the slide-count -> roll over to zero + } else if (new_active_slide_number >= this.SlideCount) { + slide_steps = 1; + } else { + this.active_slide_number = new_active_slide_number; + } + + return slide_steps; + } + + create_client_object_item_slides() { + const return_item: ClientSongItemSlides = { + title: this.item_props.Caption, + item: this.item_props.Item, + slides: [ + this.SongFile.get_title_client() + ], + slides_template: this.create_renderer_object(0) + }; + + for (const part_name of this.get_verse_order()) { + let part = undefined; + + try { + part = this.SongFile.get_part_client(part_name); + } catch (e) { + if (!(e instanceof ReferenceError)) { + throw e; + } + } + + // if a part is not available, skip it + if (part !== undefined){ + return_item.slides.push(part); + } + } + + return return_item; + } + + get active_slide(): number { + return this.active_slide_number; + } + + private validate_slide_number(slide: number): number { + const slide_count = this.SlideCount; + + if (typeof slide !== "number") { + throw new TypeError(`'${slide} is not of type 'number'`); + } + + if (slide < -slide_count || slide >= slide_count) { + throw new RangeError(`slide-number is out of range (${-slide_count}-${slide_count - 1})`); + } + + if (slide < 0) { + slide += slide_count; + } + + return slide; + } +} +function get_song_path(song_path: string): string { + return path.join(Config.path.song, song_path); +} \ No newline at end of file diff --git a/src/server/SongFile.ts b/src/server/SequenceItems/SongFile.ts similarity index 99% rename from src/server/SongFile.ts rename to src/server/SequenceItems/SongFile.ts index e328103..b710ba2 100644 --- a/src/server/SongFile.ts +++ b/src/server/SequenceItems/SongFile.ts @@ -218,7 +218,7 @@ class SongFile { } } - get title(): TitlePart { + get part_title(): TitlePart { const response: TitlePart = { type: "title", title: this.metadata.Title diff --git a/src/server/control.ts b/src/server/control.ts index 000c363..e6aa0e6 100644 --- a/src/server/control.ts +++ b/src/server/control.ts @@ -1,8 +1,8 @@ import WebSocket, { RawData } from "ws"; import Sequence from "./Sequence"; -import OSCServer, { OSCFunctionMap, OSCServerArguments } from "./osc-server"; -import WebsocketServer, { WebsocketServerArguments, WebsocketMessageHandler } from "./websocket-server"; +import OSCServer, { OSCFunctionMap, OSCServerArguments } from "./servers/osc-server"; +import WebsocketServer, { WebsocketServerArguments, WebsocketMessageHandler } from "./servers/websocket-server"; import * as JGCPSend from "./JGCPSendMessages"; import * as JGCPRecv from "./JGCPReceiveMessages"; @@ -73,13 +73,8 @@ class Control { this.send_all_clients(response_sequenceItems); - // send the selected item-slide to all clients - const respone_activeItemSlide: JGCPSend.State = { - command: "state", - activeItemSlide: this.sequence.active_item_slide - }; - - this.send_all_clients(respone_activeItemSlide); + // send the current state to all clients + this.send_all_clients(this.sequence.state); ws_send_response("sequence has been opened", true, ws); } @@ -103,7 +98,7 @@ class Control { ws_send_response("slides have been sent", true, ws); } else { - ws_send_response(`'${item} is not of type 'number''`, false, ws); + ws_send_response(`'${item} is not of type 'number'`, false, ws); } } @@ -124,23 +119,23 @@ class Control { } else { this.sequence?.set_active_item(item, slide); } - - // send the response inside the try block, so it doesn't get send in case of an error - this.send_all_clients({ - command: "state", - activeItemSlide: this.sequence.active_item_slide, - clientID - }); - - ws_send_response("slide has been selected", true, ws); } catch (e) { // catch invalid item or slide numbers if (e instanceof RangeError) { ws_send_response(e.message, true, ws); + return; } else { throw e; } } + + this.send_all_clients({ + command: "state", + activeItemSlide: this.sequence.active_item_slide, + clientID + }); + + ws_send_response("slide has been selected", true, ws); } /** @@ -233,12 +228,7 @@ class Control { ws.send(JSON.stringify(respone_sequence)); // send the selected item-slide - const respone_activeItemSlide: JGCPSend.State = { - command: "state", - activeItemSlide: this.sequence.active_item_slide - }; - - ws.send(JSON.stringify(respone_activeItemSlide)); + ws.send(JSON.stringify(this.sequence.state)); } else { // send a "clear" message to the client, so that it's currently loaded sequnece gets removed (for example after a server restart) const clear_message: JGCPSend.Clear = { diff --git a/src/server/main.ts b/src/server/main.ts index 555c4a2..66d1651 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -1,4 +1,4 @@ -import { http_server } from "./http-server"; +import { http_server } from "./servers/http-server"; import Control from "./control"; import Config from "./config"; diff --git a/src/server/http-server.ts b/src/server/servers/http-server.ts similarity index 97% rename from src/server/http-server.ts rename to src/server/servers/http-server.ts index 56d694a..5cd44de 100644 --- a/src/server/http-server.ts +++ b/src/server/servers/http-server.ts @@ -4,7 +4,7 @@ import path from "path"; import mime from "mime-types"; import { unescape } from "querystring"; -import Config from "./config"; +import Config from "../config"; class http_server { private port: number; diff --git a/src/server/osc-server.ts b/src/server/servers/osc-server.ts similarity index 100% rename from src/server/osc-server.ts rename to src/server/servers/osc-server.ts diff --git a/src/server/websocket-server.ts b/src/server/servers/websocket-server.ts similarity index 100% rename from src/server/websocket-server.ts rename to src/server/servers/websocket-server.ts diff --git a/tsconfig.json b/tsconfig.json index cf4ff5c..c43ef71 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,6 @@ "module": "NodeNext", "outDir": "out", "sourceMap": true, - "noUnusedLocals": true, "removeComments": true }, "include": [ From 629ae14fd891486a10b9a014374e3cb608619c7d Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Tue, 20 Feb 2024 19:52:07 +0100 Subject: [PATCH 03/16] Added support for countdown elements --- casparcg-templates/JohnCG/Countdown.html | 248 +++++++++++++++ casparcg-templates/JohnCG/Song.html | 7 +- client/main.js | 102 +++++-- client/song.html | 367 ----------------------- config.json | 3 +- src/server/JGCPSendMessages.ts | 4 +- src/server/Sequence.ts | 37 ++- src/server/SequenceItems/Countdown.ts | 226 ++++++++++++++ src/server/SequenceItems/SequenceItem.ts | 32 +- src/server/SequenceItems/Song.ts | 28 +- src/server/config.ts | 3 +- src/server/servers/http-server.ts | 19 +- 12 files changed, 639 insertions(+), 437 deletions(-) create mode 100644 casparcg-templates/JohnCG/Countdown.html delete mode 100644 client/song.html diff --git a/casparcg-templates/JohnCG/Countdown.html b/casparcg-templates/JohnCG/Countdown.html new file mode 100644 index 0000000..2f91dee --- /dev/null +++ b/casparcg-templates/JohnCG/Countdown.html @@ -0,0 +1,248 @@ + + + + + + JohnCG - Countdown-template + + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/casparcg-templates/JohnCG/Song.html b/casparcg-templates/JohnCG/Song.html index 9ef7b3c..8eb0df2 100644 --- a/casparcg-templates/JohnCG/Song.html +++ b/casparcg-templates/JohnCG/Song.html @@ -8,7 +8,7 @@ - johnCG - template + JohnCG - Song-template - - -
-
- -
-
-
- - - \ No newline at end of file diff --git a/config.json b/config.json index 4f76324..ee9e03c 100644 --- a/config.json +++ b/config.json @@ -8,7 +8,8 @@ }, "casparcg": { "templates": { - "song": "Song" + "Song": "JohnCG/Song", + "Countdown": "JohnCG/Countdown" }, "connections": [ { diff --git a/src/server/JGCPSendMessages.ts b/src/server/JGCPSendMessages.ts index 1bc399c..87ff026 100644 --- a/src/server/JGCPSendMessages.ts +++ b/src/server/JGCPSendMessages.ts @@ -1,5 +1,5 @@ import * as SequenceClass from "../server/Sequence"; -import { ClientItemSlides } from "./SequenceItems/SequenceItem"; +import { ClientItemSlidesBase } from "./SequenceItems/SequenceItem"; /** * Base interface for sent JGCP-messages @@ -33,7 +33,7 @@ export interface State extends Base { visibility?: boolean; } -export interface ItemSlides extends Base, ClientItemSlides { +export interface ItemSlides extends Base, ClientItemSlidesBase { clientID: string; command: "item-slides"; } diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index f0a02b0..3cf23af 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -3,11 +3,12 @@ import { CasparCG } from "casparcg-connection"; import { SongElement } from "./SequenceItems/SongFile"; import SequenceItem, { ClientItemSlides, ItemProps } from "./SequenceItems/SequenceItem"; -import { Song, SongProps } from "./SequenceItems/Song"; +import Song, { SongProps } from "./SequenceItems/Song"; import * as JGCPSend from "./JGCPSendMessages"; import Config from "./config"; +import Countdown, { CountdownProps } from "./SequenceItems/Countdown"; interface ClientSequenceItems { sequence_items: ItemProps[]; @@ -122,12 +123,11 @@ class Sequence { // store the data of the object let item_data: ItemProps = { + Type: null, Caption: "", Color: "", SlideCount: 0, - Type: null, - Item: this.sequence_items.length, - FileName: null + Item: this.sequence_items.length }; // exec the item-regex until there are no more results @@ -148,10 +148,13 @@ class Sequence { } while (re_results_item !== null); // store the sequence-item - - // TESTING only if it is song-element, since the others aren't implemented - if (item_data.Type === "Song") { - this.sequence_items.push(new Song(item_data as SongProps)); + switch (item_data.Type) { + case "Song": + this.sequence_items.push(new Song(item_data as SongProps)); + break; + case "Countdown": + this.sequence_items.push(new Countdown(item_data as CountdownProps)); + break; } }); } @@ -280,9 +283,9 @@ class Sequence { layer: Config.casparcg.connections[loop_index].layers[1], cgLayer: 0, playOnLoad: this.casparcg_visibility, - template: Config.casparcg.templates.song, + template: Config.casparcg.templates[this.active_sequence_item.props.Type], // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug - data: JSON.stringify(JSON.stringify(this.active_sequence_item.create_renderer_object(), (_key, val) => { + data: JSON.stringify(JSON.stringify(this.active_sequence_item.create_render_object(), (_key, val) => { if (typeof val === "string") { return val.replace("\"", "\\u0022"); } else { @@ -378,7 +381,7 @@ function parse_item_value_string(key: string, value: string): { [P in keyof Item break; case "VerseOrder": // split csv-line into an array - return_props.VerseOrder = value.split(",") as SongElement[]; + return_props["VerseOrder"] = value.split(",") as SongElement[]; break; case "FileName": // assume the type from the file-extension @@ -403,6 +406,16 @@ function parse_item_value_string(key: string, value: string): { [P in keyof Item case "Language": return_props[key] = Number(value) - 1; // subtract 1, because Songbeamer start counting at 1 break; + case "CaptionFmtValue": + return_props["Time"] = value; + break; + case "StreamClass": + if (value === "TPresentationObjectTimer") { + return_props.Type = "Countdown"; + } else { + return_props[key] = value; + } + break; default: return_props[key] = value; break; @@ -560,5 +573,5 @@ function convert_color_to_hex(color: string): string | undefined { } // export { NavigateType, isItemNavigateType, ClientSequenceItems, ClientItemSlides, ActiveItemSlide }; -export { ClientSequenceItems, ActiveItemSlide }; +export { ClientSequenceItems, ActiveItemSlide, convert_color_to_hex }; export default Sequence; diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts index e69de29..477407d 100644 --- a/src/server/SequenceItems/Countdown.ts +++ b/src/server/SequenceItems/Countdown.ts @@ -0,0 +1,226 @@ +import { convert_color_to_hex } from "../Sequence"; +import SequenceItem, { ClientItemSlidesBase, FontFormat, ItemPropsBase, ItemRenderObjectBase, get_image_b64 } from "./SequenceItem"; + +const countdown_mode_items = ["duration", "end-time", "stopwatch", "clock"]; +type CountdownMode = (typeof countdown_mode_items)[number]; + +interface CountdownPosition { x: number, y: number } + +interface CountdownSequenceItemProps extends ItemPropsBase { + Type: "Countdown"; + Time: string; + Data: string; +} + +export interface CountdownProps extends CountdownSequenceItemProps { + FontFormat: FontFormat; + Position: CountdownPosition; + Mode: CountdownMode; + showSeconds: boolean; + BackgroundImage?: string; + BackgroundColor?: string; +} + +export interface ClientCountdownSlides extends ClientItemSlidesBase { + Type: "Countdown"; + slides: [{ + time: string; + mode: CountdownMode; + }], + slides_template: CountdownRenderObject; +} + +export interface CountdownRenderObject extends ItemRenderObjectBase { + slides: undefined; + position: CountdownPosition; + fontFormat: FontFormat; + time: string; + showSeconds: boolean; +} + +// data from in the hex-string of the countdown, uses CSS-notation for easy translation in the renderer +interface CountdownData { + mode: CountdownMode; + fontFormat: { + fontFamily?: string; + fontSize: number; + color: string; + fontWeight?: "bold"; + fontStyle?: "italic"; + textDecoration?: "underline"; + } + x: number; + y: number; + showSeconds: boolean; + backgroundImage?: string; + backgroundColor?: string; +} + +export default class Countdown extends SequenceItem { + protected item_props: CountdownProps; + + constructor(props: CountdownSequenceItemProps) { + super(); + + const hex_data = parse_hex_data(props.Data); + + this.item_props = { + ...props, + Position: { + x: hex_data.x, + y: hex_data.y + }, + showSeconds: hex_data.showSeconds, + FontFormat: hex_data.fontFormat, + Mode: hex_data.mode, + BackgroundImage: hex_data.backgroundImage, + BackgroundColor: hex_data.backgroundColor + }; + } + + navigate_slide(steps: number): number { + if (typeof steps !== "number") { + throw new TypeError(`steps ('${steps}') is no number`); + } + + if (![-1, 1].includes(steps)) { + throw new RangeError(`steps must be -1 or 1, but is ${steps}`); + } + + // directly return the steps as item-navigation-steps, since this can't be navigated + return steps; + } + + set_active_slide(slide?: number): number { + if (typeof slide !== "number") { + throw new TypeError(`slide ('${slide}') is no number`); + } + + if (slide !== 0) { + throw new RangeError(`slide ('${slide}') is out of range (0)`); + } + + return slide; + } + + create_client_object_item_slides(): ClientCountdownSlides { + const title_map = { + clock: "Clock", + stopwatch: "Stopwatch", + duration: "Countdown (endtime)", + "end-time": "Countdown (duration)" + }; + + return { + title: `${title_map[this.props.Mode]}: ${this.props.Time}`, + Type: this.props.Type, + item: this.props.Item, + slides: [{ + mode: this.props.Mode, + time: this.props.Time + }], + slides_template: this.create_render_object(), + }; + } + + create_render_object(): CountdownRenderObject { + return { + backgroundImage: get_image_b64(this.props.BackgroundImage), + backgroundColor: this.props.BackgroundColor, + slide: 0, + slides: undefined, + time: this.props.Time, + fontFormat: this.props.FontFormat, + position: this.props.Position, + showSeconds: this.props.showSeconds + }; + } + + get active_slide(): number { + // always return 0, because there is only 1 slide + return 0; + } + + get props(): CountdownProps { + return this.item_props; + } +} + +function parse_hex_data(data_hex): CountdownData { + const regex_curse = /(?:546578745374796C6573060[1-3](?42)?(?49)?(?55)?|54657874436F6C6F72(?:(?:04|0707)(?[0-9A-F]{6}|(?:[0-9A-F]{2})+?)0)|466F6E744E616D650604(?(?:[A-F0-9]{2})+?)09|547970020(?[0-3])09|506F736974696F6E5802(?[A-F0-9]{2})09|506F736974696F6E5902(?[A-F0-9]{2})08|466F6E7453697A6502(?[A-F0-9]{2})0F|4261636B67726F756E64496D616765(?:[A-F0-9]{4}636F6C6F723A2F2F244646(?[A-F0-9]{12})|(?:[A-F0-9]{2})*?0[0-F]{3}(?(?:[0-9A-F]{2})+?))0|53686F775365636F6E647308(?.))/g; + + + const data: CountdownData = { + fontFormat: { + color: undefined, + fontSize: undefined + }, + mode: undefined, + x: undefined, + y: undefined, + showSeconds: true + }; + + const to_string = (raw) => Buffer.from(raw, "hex").toString(); + + const to_int = (r) => parseInt(r, 16); + + const to_rgb = (r) => { + // if it is longer than 6 bytes, it is an colorName + if (r.length > 6) { + return convert_color_to_hex(to_string(r)); + } else { + return `#${r}`; + } + }; + + // regex match the data + for (const res of data_hex.matchAll(regex_curse)) { + // parse the results and add them to the results-object + Object.entries(res.groups).forEach(([key, val]) => { + if (val !== undefined) { + switch (key) { + case "mode": + data.mode = countdown_mode_items[Number(val)]; + break; + case "color": + data.fontFormat.color = to_rgb(val); + break; + case "fontSize": + data.fontFormat.fontSize = to_int(val); + break; + case "x": + case "y": + data[key] = to_int(val); + break; + case "showSeconds": + data.showSeconds = !val; + break; + case "bold": + data.fontFormat.fontWeight = "bold"; + break; + case "italic": + data.fontFormat.fontStyle = "italic"; + break; + case "underline": + data.fontFormat.textDecoration = "underline"; + break; + case "fontFamily": + data.fontFormat.fontFamily = to_string(val); + break; + case "backgroundImage": + data.backgroundImage = to_string(val); + break; + case "backgroundColor": + data.backgroundColor = `#${to_string(val)}`; + break; + default: + console.error(`countdown_data['${key}'] is not implemented yet`); + break; + } + } + }); + } + + return data; +} \ No newline at end of file diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index d88de50..d778a27 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -3,38 +3,56 @@ import path from "path"; import mime from "mime-types"; import Config from "../config"; -import { SongProps } from "./Song"; +import { ClientSongSlides, SongProps, SongRenderObject } from "./Song"; +import { ClientCountdownSlides, CountdownProps, CountdownRenderObject } from "./Countdown"; export interface ItemPropsBase { + Type: string; Caption: string; SlideCount: number; Color: string; Item: number; } -export type ItemProps = SongProps +export type ItemProps = ItemPropsBase | SongProps | CountdownProps // interface for a renderer-object -export interface RenderObject { - slides: object; +export interface ItemRenderObjectBase { + slides: Array; slide: number; backgroundImage?: string; + backgroundColor?: string; } -export interface ClientItemSlides { +export type ItemRenderObject = ItemRenderObjectBase | SongRenderObject | CountdownRenderObject; + +export interface ClientItemSlidesBase { + Type: string; title: string; item: number; slides: object; - slides_template: RenderObject; + slides_template: ItemRenderObject; +} + +export type ClientItemSlides = ClientItemSlidesBase | ClientSongSlides | ClientCountdownSlides; + +export interface FontFormat { + fontFamily?: string; + fontSize: number; + fontWeight?: "bold"; + fontStyle?: "italic"; + fontDecoration?: "underline"; + color: string; } export default abstract class SequenceItem { protected abstract item_props: ItemProps; - abstract create_renderer_object(slide?: number); + abstract create_render_object(slide?: number); abstract create_client_object_item_slides(): ClientItemSlides; abstract set_active_slide(slide?: number): number; + /** * navigate the selected slide * @param steps steps to navigate; sign is used for direction diff --git a/src/server/SequenceItems/Song.ts b/src/server/SequenceItems/Song.ts index 11e74e9..bf31690 100644 --- a/src/server/SequenceItems/Song.ts +++ b/src/server/SequenceItems/Song.ts @@ -1,4 +1,4 @@ -import SequenceItem, { ClientItemSlides, ItemPropsBase, RenderObject, get_image_b64 } from "./SequenceItem"; +import SequenceItem, { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, get_image_b64 } from "./SequenceItem"; import SongFile, { ItemPartClient, LyricPart, TitlePart } from "./SongFile"; import path from "path"; @@ -22,18 +22,19 @@ interface LyricSlide { type ItemSlide = LyricSlide | TitleSlide; -interface SongRenderObject extends RenderObject { +export interface SongRenderObject extends ItemRenderObjectBase { type: "Song"; slides: ItemSlide[]; languages: number[]; } -export interface ClientSongItemSlides extends ClientItemSlides { +export interface ClientSongSlides extends ClientItemSlidesBase { + Type: "Song" slides: ItemPartClient[]; - slides_template: RenderObject; + slides_template: SongRenderObject; } -export class Song extends SequenceItem { +export default class Song extends SequenceItem { protected item_props: SongProps; // amount of slides this element has @@ -45,17 +46,17 @@ export class Song extends SequenceItem { private SongFile: SongFile; - constructor(data: SongProps) { + constructor(props: SongProps) { super(); - this.item_props = data; + this.item_props = props; try { - this.SongFile = new SongFile(get_song_path(data.FileName)); + this.SongFile = new SongFile(get_song_path(props.FileName)); } catch (e) { // if the error is because the file doesn't exist, skip the rest of the loop iteration if (e.code === "ENOENT") { - console.debug(`song '${data.FileName}' does not exist`); + console.debug(`song '${props.FileName}' does not exist`); return null; } else { throw e; @@ -101,7 +102,7 @@ export class Song extends SequenceItem { } } - create_renderer_object(slide: number): SongRenderObject { + create_render_object(slide: number): SongRenderObject { if (slide === undefined) { slide = this.active_slide; } @@ -184,13 +185,14 @@ export class Song extends SequenceItem { } create_client_object_item_slides() { - const return_item: ClientSongItemSlides = { + const return_item: ClientSongSlides = { + Type: "Song", title: this.item_props.Caption, item: this.item_props.Item, slides: [ this.SongFile.get_title_client() ], - slides_template: this.create_renderer_object(0) + slides_template: this.create_render_object(0) }; for (const part_name of this.get_verse_order()) { @@ -237,4 +239,4 @@ export class Song extends SequenceItem { } function get_song_path(song_path: string): string { return path.join(Config.path.song, song_path); -} \ No newline at end of file +} diff --git a/src/server/config.ts b/src/server/config.ts index aa86a0d..10511db 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -8,7 +8,8 @@ interface ConfigJSON { }; casparcg: { templates: { - song: string + Song: string, + Countdown: string }; connections: { host: string; diff --git a/src/server/servers/http-server.ts b/src/server/servers/http-server.ts index 5cd44de..9587f61 100644 --- a/src/server/servers/http-server.ts +++ b/src/server/servers/http-server.ts @@ -23,16 +23,21 @@ class http_server { request.url = unescape(request.url); // override different requested urls - switch (request.url) { + switch (true) { // redirect the root to the main-site - case "/": + case /^\/$/.test(request.url): request.url = "main.html"; break; - default: - if (/\/BackgroundImage\/.*/.test(request.url)) { - resource_dir = Config.path.backgroundImage; - request.url = request.url.replace(/\/BackgroundImage\//, ""); - } + // serve the casparcg-templates + case /^\/Templates\//.test(request.url): + resource_dir = "casparcg-templates/JohnCG"; + request.url = request.url.replace(/\/Templates\//, ""); + break; + // serve the background-images + case /^\/BackgroundImage\//.test(request.url): + resource_dir = Config.path.backgroundImage; + request.url = request.url.replace(/\/BackgroundImage\//, ""); + break; } // try to serve the requested url From 8763d1e4c4721f023eead44c8162861c35eb7565 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Tue, 20 Feb 2024 22:25:40 +0100 Subject: [PATCH 04/16] Fixed a bug in the slide-navigation --- src/server/Sequence.ts | 9 +++--- src/server/SequenceItems/Countdown.ts | 36 +++++++++++------------- src/server/SequenceItems/SequenceItem.ts | 21 +++++++++++++- src/server/SequenceItems/Song.ts | 24 ++-------------- 4 files changed, 44 insertions(+), 46 deletions(-) diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index 3cf23af..b694c6c 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -2,7 +2,7 @@ import path from "path"; import { CasparCG } from "casparcg-connection"; import { SongElement } from "./SequenceItems/SongFile"; -import SequenceItem, { ClientItemSlides, ItemProps } from "./SequenceItems/SequenceItem"; +import SequenceItemBase, { ClientItemSlides, ItemProps } from "./SequenceItems/SequenceItem"; import Song, { SongProps } from "./SequenceItems/Song"; import * as JGCPSend from "./JGCPSendMessages"; @@ -25,7 +25,7 @@ interface ActiveItemSlide { class Sequence { // store the individual items of the sequence - sequence_items: SequenceItem[] = []; + sequence_items: SequenceItemBase[] = []; private active_item_number: number = 0; @@ -232,7 +232,8 @@ class Sequence { const item_steps = this.active_sequence_item.navigate_slide(steps); if (item_steps !== 0) { - this.navigate_item(steps); + // if the item_steps is forwards, navigate to the first slide; if it is backwards navigate to the last one + this.navigate_item(steps, steps > 0 ? 0 : -1); } else { this.casparcg_select_slide(this.active_sequence_item.active_slide); } @@ -336,7 +337,7 @@ class Sequence { return this.active_item_number; } - get active_sequence_item(): SequenceItem { + get active_sequence_item(): SequenceItemBase { return this.sequence_items[this.active_item_number]; } diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts index 477407d..e264f61 100644 --- a/src/server/SequenceItems/Countdown.ts +++ b/src/server/SequenceItems/Countdown.ts @@ -1,5 +1,5 @@ import { convert_color_to_hex } from "../Sequence"; -import SequenceItem, { ClientItemSlidesBase, FontFormat, ItemPropsBase, ItemRenderObjectBase, get_image_b64 } from "./SequenceItem"; +import SequenceItemBase, { ClientItemSlidesBase, FontFormat, ItemPropsBase, ItemRenderObjectBase, get_image_b64 } from "./SequenceItem"; const countdown_mode_items = ["duration", "end-time", "stopwatch", "clock"]; type CountdownMode = (typeof countdown_mode_items)[number]; @@ -56,8 +56,10 @@ interface CountdownData { backgroundColor?: string; } -export default class Countdown extends SequenceItem { +export default class Countdown extends SequenceItemBase { protected item_props: CountdownProps; + + protected SlideCount: number = 1; constructor(props: CountdownSequenceItemProps) { super(); @@ -92,13 +94,7 @@ export default class Countdown extends SequenceItem { } set_active_slide(slide?: number): number { - if (typeof slide !== "number") { - throw new TypeError(`slide ('${slide}') is no number`); - } - - if (slide !== 0) { - throw new RangeError(`slide ('${slide}') is out of range (0)`); - } + this.validate_slide_number(slide); return slide; } @@ -112,12 +108,12 @@ export default class Countdown extends SequenceItem { }; return { - title: `${title_map[this.props.Mode]}: ${this.props.Time}`, - Type: this.props.Type, - item: this.props.Item, + title: `${title_map[this.item_props.Mode]}: ${this.item_props.Time}`, + Type: this.item_props.Type, + item: this.item_props.Item, slides: [{ - mode: this.props.Mode, - time: this.props.Time + mode: this.item_props.Mode, + time: this.item_props.Time }], slides_template: this.create_render_object(), }; @@ -125,14 +121,14 @@ export default class Countdown extends SequenceItem { create_render_object(): CountdownRenderObject { return { - backgroundImage: get_image_b64(this.props.BackgroundImage), - backgroundColor: this.props.BackgroundColor, + backgroundImage: get_image_b64(this.item_props.BackgroundImage), + backgroundColor: this.item_props.BackgroundColor, slide: 0, slides: undefined, - time: this.props.Time, - fontFormat: this.props.FontFormat, - position: this.props.Position, - showSeconds: this.props.showSeconds + time: this.item_props.Time, + fontFormat: this.item_props.FontFormat, + position: this.item_props.Position, + showSeconds: this.item_props.showSeconds }; } diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index d778a27..9e51d8e 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -46,8 +46,9 @@ export interface FontFormat { color: string; } -export default abstract class SequenceItem { +export default abstract class SequenceItemBase { protected abstract item_props: ItemProps; + protected abstract SlideCount: number; abstract create_render_object(slide?: number); abstract create_client_object_item_slides(): ClientItemSlides; @@ -62,6 +63,24 @@ export default abstract class SequenceItem { abstract get active_slide(): number; + protected validate_slide_number(slide: number): number { + const slide_count = this.SlideCount; + + if (typeof slide !== "number") { + throw new TypeError(`'${slide} is not of type 'number'`); + } + + if (slide < -slide_count || slide >= slide_count) { + throw new RangeError(`slide-number is out of range (${-slide_count}-${slide_count - 1})`); + } + + if (slide < 0) { + slide += slide_count; + } + + return slide; + } + get props(): ItemProps { return this.item_props; } diff --git a/src/server/SequenceItems/Song.ts b/src/server/SequenceItems/Song.ts index bf31690..a1749a2 100644 --- a/src/server/SequenceItems/Song.ts +++ b/src/server/SequenceItems/Song.ts @@ -1,4 +1,4 @@ -import SequenceItem, { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, get_image_b64 } from "./SequenceItem"; +import SequenceItemBase, { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, get_image_b64 } from "./SequenceItem"; import SongFile, { ItemPartClient, LyricPart, TitlePart } from "./SongFile"; import path from "path"; @@ -34,11 +34,11 @@ export interface ClientSongSlides extends ClientItemSlidesBase { slides_template: SongRenderObject; } -export default class Song extends SequenceItem { +export default class Song extends SequenceItemBase { protected item_props: SongProps; // amount of slides this element has - private SlideCount: number = 0; + protected SlideCount: number = 0; // currently active slide-number private active_slide_number: number = 0; @@ -218,24 +218,6 @@ export default class Song extends SequenceItem { get active_slide(): number { return this.active_slide_number; } - - private validate_slide_number(slide: number): number { - const slide_count = this.SlideCount; - - if (typeof slide !== "number") { - throw new TypeError(`'${slide} is not of type 'number'`); - } - - if (slide < -slide_count || slide >= slide_count) { - throw new RangeError(`slide-number is out of range (${-slide_count}-${slide_count - 1})`); - } - - if (slide < 0) { - slide += slide_count; - } - - return slide; - } } function get_song_path(song_path: string): string { return path.join(Config.path.song, song_path); From 455bf3e64200a1d96c45cc2837fa6139d97b68ed Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Wed, 21 Feb 2024 01:19:51 +0100 Subject: [PATCH 05/16] added support for comments --- client/main.css | 8 ++-- client/main.js | 13 ++++-- src/server/Sequence.ts | 51 +++++++++++++++++++++--- src/server/SequenceItems/Comment.ts | 44 ++++++++++++++++++++ src/server/SequenceItems/Countdown.ts | 2 +- src/server/SequenceItems/SequenceItem.ts | 9 +++-- src/server/SequenceItems/Song.ts | 4 +- 7 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 src/server/SequenceItems/Comment.ts diff --git a/client/main.css b/client/main.css index a9becd8..176629c 100644 --- a/client/main.css +++ b/client/main.css @@ -163,7 +163,9 @@ div.button, div.button > * { display: flex; align-items: stretch; - +} + +.sequence_item_container.selectable { cursor: pointer; } @@ -189,7 +191,7 @@ div.button, div.button > * { text-wrap: nowrap; } -.sequence_item_container:hover > .sequence_item { +.sequence_item_container.selectable:hover > .sequence_item { background-color: rgb(79, 83, 94); } @@ -197,7 +199,7 @@ div.button, div.button > * { background-color: rgb(40, 76, 184); } -.sequence_item_container.active:hover > .sequence_item { +.sequence_item_container.selectable.active:hover > .sequence_item { background-color: rgb(54, 92, 192); } diff --git a/client/main.js b/client/main.js index 6be2ffe..01569fb 100644 --- a/client/main.js +++ b/client/main.js @@ -55,6 +55,15 @@ function display_items(data) { div_sequence_item_container.classList.add("sequence_item_container"); div_sequence_item_container.dataset.item_number = item.Item; + // if the item is selectable, give it the class and add the onclic-event + if (item.selectable) { + div_sequence_item_container.classList.add("selectable"); + + div_sequence_item_container.onclick = function() { + request_item_slides(Number(this.dataset.item_number)); + }; + } + const div_sequence_item_color_indicator = document.createElement("div"); div_sequence_item_color_indicator.classList.add("item_color_indicator"); div_sequence_item_color_indicator.style.backgroundColor = item.Color; @@ -71,10 +80,6 @@ function display_items(data) { div_sequence_item.innerText = item.Caption; } - div_sequence_item_container.onclick = function() { - request_item_slides(Number(this.dataset.item_number)); - }; - div_sequence_item_container.append(div_sequence_item); div_sequence_items.append(div_sequence_item_container); } diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index b694c6c..fc48dc5 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -2,13 +2,14 @@ import path from "path"; import { CasparCG } from "casparcg-connection"; import { SongElement } from "./SequenceItems/SongFile"; -import SequenceItemBase, { ClientItemSlides, ItemProps } from "./SequenceItems/SequenceItem"; +import { ClientItemSlides, ItemProps, SequenceItem } from "./SequenceItems/SequenceItem"; import Song, { SongProps } from "./SequenceItems/Song"; import * as JGCPSend from "./JGCPSendMessages"; import Config from "./config"; import Countdown, { CountdownProps } from "./SequenceItems/Countdown"; +import Comment, { CommentProps } from "./SequenceItems/Comment"; interface ClientSequenceItems { sequence_items: ItemProps[]; @@ -25,7 +26,7 @@ interface ActiveItemSlide { class Sequence { // store the individual items of the sequence - sequence_items: SequenceItemBase[] = []; + sequence_items: SequenceItem[] = []; private active_item_number: number = 0; @@ -127,7 +128,8 @@ class Sequence { Caption: "", Color: "", SlideCount: 0, - Item: this.sequence_items.length + Item: this.sequence_items.length, + selectable: true }; // exec the item-regex until there are no more results @@ -155,6 +157,12 @@ class Sequence { case "Countdown": this.sequence_items.push(new Countdown(item_data as CountdownProps)); break; + default: + // if it wasn't caught by other cases, it is either a comment or not implemented yet -> if there is no file specified, treat it as comment + if (!Object.keys(item_data).includes("FileName")) { + this.sequence_items.push(new Comment(item_data as CommentProps)); + } + break; } }); } @@ -164,7 +172,6 @@ class Sequence { sequence_items: this.sequence_items.map((item) => item.props), metadata: { item: this.active_item_number, - // slide: this.active_slide, visibility: this.visibility } }; @@ -211,6 +218,19 @@ class Sequence { } let new_active_item_number = this.active_item_number + steps; + // steps until there is a selectable item + while (!this.sequence_items[new_active_item_number].props.selectable) { + new_active_item_number += steps; + + // if the new_active_item_number is back at the start, break, since there are no selectable items + if (new_active_item_number === this.active_item_number) { + console.error("loop around"); + return; + } + + // sanitize the item-number + new_active_item_number = this.sanitize_item_number(new_active_item_number); + } // new active item has negative index -> roll over to other end if (new_active_item_number < 0) { @@ -255,6 +275,27 @@ class Sequence { return item; } + /** + * sanitize the item-number by over- / underrolling it. + * @param item + * @returns sanitized number; active_item_number if no integer was given + */ + private sanitize_item_number(item: number): number { + if (!Number.isInteger(item)) { + return this.active_item; + } + + // clamp the range + item = item % this.sequence_items.length; + + // if it is negative, roll over + if (item < 0) { + item += this.sequence_items.length; + } + + return item; + } + private casparcg_load_item(item: number, index?: number, flip_layer: boolean = true): void { if (flip_layer) { // clear the lower layer @@ -337,7 +378,7 @@ class Sequence { return this.active_item_number; } - get active_sequence_item(): SequenceItemBase { + get active_sequence_item(): SequenceItem { return this.sequence_items[this.active_item_number]; } diff --git a/src/server/SequenceItems/Comment.ts b/src/server/SequenceItems/Comment.ts new file mode 100644 index 0000000..0af4e14 --- /dev/null +++ b/src/server/SequenceItems/Comment.ts @@ -0,0 +1,44 @@ +import { ClientItemSlides, ItemProps, ItemPropsBase, SequenceItemBase } from "./SequenceItem"; + +export interface CommentProps extends ItemPropsBase { + selectable: false; +} + +export default class Comment extends SequenceItemBase { + protected item_props: CommentProps; + + protected SlideCount: number; + + constructor(props: CommentProps) { + super(); + + this.item_props = props; + + this.item_props.selectable = false; + } + + create_client_object_item_slides(): ClientItemSlides { + return undefined; + } + + create_render_object() { + return undefined; + } + + navigate_slide(steps: number): number { + // return the steps, since there are no slides to navigate + return steps; + } + + set_active_slide(): number { + return undefined; + } + + get active_slide(): number { + return undefined; + } + + get props(): ItemProps { + return this.item_props; + } +} \ No newline at end of file diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts index e264f61..4f8854e 100644 --- a/src/server/SequenceItems/Countdown.ts +++ b/src/server/SequenceItems/Countdown.ts @@ -1,5 +1,5 @@ import { convert_color_to_hex } from "../Sequence"; -import SequenceItemBase, { ClientItemSlidesBase, FontFormat, ItemPropsBase, ItemRenderObjectBase, get_image_b64 } from "./SequenceItem"; +import { ClientItemSlidesBase, FontFormat, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase, get_image_b64 } from "./SequenceItem"; const countdown_mode_items = ["duration", "end-time", "stopwatch", "clock"]; type CountdownMode = (typeof countdown_mode_items)[number]; diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index 9e51d8e..882473b 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -3,9 +3,11 @@ import path from "path"; import mime from "mime-types"; import Config from "../config"; -import { ClientSongSlides, SongProps, SongRenderObject } from "./Song"; -import { ClientCountdownSlides, CountdownProps, CountdownRenderObject } from "./Countdown"; +import Song, { ClientSongSlides, SongProps, SongRenderObject } from "./Song"; +import Countdown, { ClientCountdownSlides, CountdownProps, CountdownRenderObject } from "./Countdown"; +import Comment from "./Comment"; +export type SequenceItem = Song | Countdown | Comment; export interface ItemPropsBase { Type: string; @@ -13,6 +15,7 @@ export interface ItemPropsBase { SlideCount: number; Color: string; Item: number; + selectable: boolean; } export type ItemProps = ItemPropsBase | SongProps | CountdownProps @@ -46,7 +49,7 @@ export interface FontFormat { color: string; } -export default abstract class SequenceItemBase { +export abstract class SequenceItemBase { protected abstract item_props: ItemProps; protected abstract SlideCount: number; diff --git a/src/server/SequenceItems/Song.ts b/src/server/SequenceItems/Song.ts index a1749a2..a27a6f5 100644 --- a/src/server/SequenceItems/Song.ts +++ b/src/server/SequenceItems/Song.ts @@ -1,4 +1,4 @@ -import SequenceItemBase, { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, get_image_b64 } from "./SequenceItem"; +import { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase, get_image_b64 } from "./SequenceItem"; import SongFile, { ItemPartClient, LyricPart, TitlePart } from "./SongFile"; import path from "path"; @@ -102,7 +102,7 @@ export default class Song extends SequenceItemBase { } } - create_render_object(slide: number): SongRenderObject { + create_render_object(slide?: number): SongRenderObject { if (slide === undefined) { slide = this.active_slide; } From c2208f67420eedbf83d729821ac26ffa6abb1b73 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Wed, 21 Feb 2024 05:14:52 +0100 Subject: [PATCH 06/16] improved client-performance (no more iframes) and started using async --- README.md | 5 +- casparcg-templates/JohnCG/Countdown.html | 11 +- casparcg-templates/JohnCG/Song.html | 13 +- client/main.js | 38 ++--- package.json | 1 + src/server/Sequence.ts | 4 +- src/server/SequenceItems/Comment.ts | 10 +- src/server/SequenceItems/Countdown.ts | 34 ++++- src/server/SequenceItems/SequenceItem.ts | 44 ++++-- src/server/SequenceItems/Song.ts | 28 ++-- src/server/control.ts | 4 +- yarn.lock | 186 ++++++++++++++++++++++- 12 files changed, 314 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 7703d14..0a97254 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ Generate lyric-graphics and play them out through CasparCG. ## roadmap - implement other sequence-items than song - client: information about connection (active / reconnecting / ...) -- use async / await - companion integration (buttons for song parts -> send name to casparcg) - try to get the template and client to use the settings file (CSS has default values, send song data overwrites them) - check client -> server slide_number out of range @@ -18,10 +17,10 @@ Generate lyric-graphics and play them out through CasparCG. - create message log / show error messages - write installation instruction - client communication with osc over websocket? -- client: don't use iframes, include html directly / alternatively: clone iframes instead of creating new ones - add support for NodeCG - look into document fragments - add CLI output to server - CasparCG: split text and image in two layers: enables text without background - rewrite client in typescript -- create dummy-sequence-items for unsupported ones \ No newline at end of file +- create dummy-sequence-items for unsupported ones +- disable buttons, when no sequence is loaded \ No newline at end of file diff --git a/casparcg-templates/JohnCG/Countdown.html b/casparcg-templates/JohnCG/Countdown.html index 2f91dee..e7f229e 100644 --- a/casparcg-templates/JohnCG/Countdown.html +++ b/casparcg-templates/JohnCG/Countdown.html @@ -86,9 +86,16 @@ function update(str_args) { // clear the old-state clearInterval(updateInterval); - + data = JSON.parse(str_args); - + + // if requested, diable transition-effects + if (data.mute_transition) { + document.querySelector("#main").style.transitionProperty = "none"; + } else { + document.querySelector("#main").style.transitionProperty = ""; + } + // create the individual spans for the numbers spans.hours = [ document.createElement("span"), diff --git a/casparcg-templates/JohnCG/Song.html b/casparcg-templates/JohnCG/Song.html index 8eb0df2..04fb604 100644 --- a/casparcg-templates/JohnCG/Song.html +++ b/casparcg-templates/JohnCG/Song.html @@ -194,10 +194,19 @@ function update(s_data) { // parse the transferred data into json data = JSON.parse(s_data); - + // get the div for the display and storage const div_container = document.querySelector("#container"); const div_storage = document.querySelector("#storage"); + + div_container.style.backgroundImage = `url("${data.backgroundImage}")`; + + // if requested, diable transition-effects + if (data.mute_transition) { + document.querySelector("#_main").style.transitionProperty = "none"; + } else { + document.querySelector("#_main").style.transitionProperty = ""; + } // clear the storage-container div_storage.innerHTML = ""; @@ -218,8 +227,6 @@ // store the amount of slides slide_count = slide_counter; - div_container.style.backgroundImage = `url("${data.backgroundImage.replace(/\\/g, "\\\\")}")`; - active_slide = data.slide; // display the first slide diff --git a/client/main.js b/client/main.js index 01569fb..1519649 100644 --- a/client/main.js +++ b/client/main.js @@ -169,17 +169,17 @@ function create_song_slides(data) { } break; } - let iframe_iter_array = [...Array(part.slides).keys()]; + let object_iter_array = [...Array(part.slides).keys()]; - iframe_iter_array = iframe_iter_array.map((ii) => { + object_iter_array = object_iter_array.map((ii) => { return { index: ii, - slide: create_slide_iframe(data, slides_start + ii) + slide: create_slide_object(data, slides_start + ii) }; }); - iframe_iter_array.forEach((iframe) => { - div_slides_view.append(iframe.slide); + object_iter_array.forEach((obj) => { + div_slides_view.append(obj.slide); }); return div_slide_part; @@ -205,47 +205,47 @@ function create_countdown_slides(data) { div_slide_part_header.innerText = data.title; - const iframe = create_slide_iframe(data, 0); + const obj = create_slide_object(data, 0); - div_slides_view.append(iframe); + div_slides_view.append(obj); return [div_slide_part]; } -function create_slide_iframe(data, number) { +function create_slide_object(data, number) { const div_slide_container = document.createElement("div"); div_slide_container.classList.add("slide_container"); - const slide_iframe = document.createElement("iframe"); + const slide_object = document.createElement("object"); switch (data.Type) { case "Song": - slide_iframe.src = "Templates/Song.html"; + slide_object.data = "Templates/Song.html"; break; case "Countdown": - slide_iframe.src = "Templates/Countdown.html" + slide_object.data = "Templates/Countdown.html" break; } - slide_iframe.classList.add("slide"); + slide_object.classList.add("slide"); - div_slide_container.append(slide_iframe); + div_slide_container.append(slide_object); - slide_iframe.dataset.slide_number = number; + slide_object.dataset.slide_number = number; - slide_iframe.addEventListener("load", () => { - slide_iframe.contentWindow.update(JSON.stringify(data.slides_template)); + slide_object.addEventListener("load", () => { + slide_object.contentWindow.update(JSON.stringify(data.slides_template)); switch (data.Type) { case "Song": - slide_iframe.contentWindow.jump(number); + slide_object.contentWindow.jump(number); break; } - slide_iframe.contentWindow.play(); + slide_object.contentWindow.play(); // register click event - slide_iframe.contentWindow.addEventListener("click", () => { + slide_object.contentWindow.addEventListener("click", () => { request_item_slide_select( Number(document.querySelector(".sequence_item_container.selected").dataset.item_number), number diff --git a/package.json b/package.json index a6d54c0..3aa798b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "iconv-lite": "^0.6.3", "mime-types": "^2.1.35", "osc": "^2.4.4", + "sharp": "^0.33.2", "ws": "^8.16.0" }, "devDependencies": { diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index fc48dc5..540f140 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -179,7 +179,7 @@ class Sequence { return return_sequence; } - create_client_object_item_slides(item: number): ClientItemSlides { + async create_client_object_item_slides(item: number): Promise { return this.sequence_items[item].create_client_object_item_slides(); } @@ -327,7 +327,7 @@ class Sequence { playOnLoad: this.casparcg_visibility, template: Config.casparcg.templates[this.active_sequence_item.props.Type], // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug - data: JSON.stringify(JSON.stringify(this.active_sequence_item.create_render_object(), (_key, val) => { + data: JSON.stringify(JSON.stringify(await this.active_sequence_item.create_render_object(), (_key, val) => { if (typeof val === "string") { return val.replace("\"", "\\u0022"); } else { diff --git a/src/server/SequenceItems/Comment.ts b/src/server/SequenceItems/Comment.ts index 0af4e14..a960e0a 100644 --- a/src/server/SequenceItems/Comment.ts +++ b/src/server/SequenceItems/Comment.ts @@ -1,4 +1,4 @@ -import { ClientItemSlides, ItemProps, ItemPropsBase, SequenceItemBase } from "./SequenceItem"; +import { ClientItemSlides, ItemProps, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; export interface CommentProps extends ItemPropsBase { selectable: false; @@ -17,11 +17,11 @@ export default class Comment extends SequenceItemBase { this.item_props.selectable = false; } - create_client_object_item_slides(): ClientItemSlides { + async create_client_object_item_slides(): Promise { return undefined; } - create_render_object() { + async create_render_object(): Promise { return undefined; } @@ -33,6 +33,10 @@ export default class Comment extends SequenceItemBase { set_active_slide(): number { return undefined; } + + protected async get_background_image(): Promise { + return undefined; + } get active_slide(): number { return undefined; diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts index 4f8854e..6266be0 100644 --- a/src/server/SequenceItems/Countdown.ts +++ b/src/server/SequenceItems/Countdown.ts @@ -1,5 +1,5 @@ import { convert_color_to_hex } from "../Sequence"; -import { ClientItemSlidesBase, FontFormat, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase, get_image_b64 } from "./SequenceItem"; +import { ClientItemSlidesBase, FontFormat, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; const countdown_mode_items = ["duration", "end-time", "stopwatch", "clock"]; type CountdownMode = (typeof countdown_mode_items)[number]; @@ -17,7 +17,11 @@ export interface CountdownProps extends CountdownSequenceItemProps { Position: CountdownPosition; Mode: CountdownMode; showSeconds: boolean; - BackgroundImage?: string; + BackgroundImage?: { + path: string; + orig?: string; + proxy?: string; + }; BackgroundColor?: string; } @@ -27,7 +31,7 @@ export interface ClientCountdownSlides extends ClientItemSlidesBase { time: string; mode: CountdownMode; }], - slides_template: CountdownRenderObject; + slides_template: CountdownRenderObject & { mute_transition: true; }; } export interface CountdownRenderObject extends ItemRenderObjectBase { @@ -75,7 +79,9 @@ export default class Countdown extends SequenceItemBase { showSeconds: hex_data.showSeconds, FontFormat: hex_data.fontFormat, Mode: hex_data.mode, - BackgroundImage: hex_data.backgroundImage, + BackgroundImage: { + path: hex_data.backgroundImage + }, BackgroundColor: hex_data.backgroundColor }; } @@ -99,7 +105,7 @@ export default class Countdown extends SequenceItemBase { return slide; } - create_client_object_item_slides(): ClientCountdownSlides { + async create_client_object_item_slides(): Promise { const title_map = { clock: "Clock", stopwatch: "Stopwatch", @@ -115,13 +121,16 @@ export default class Countdown extends SequenceItemBase { mode: this.item_props.Mode, time: this.item_props.Time }], - slides_template: this.create_render_object(), + slides_template: { + ...await this.create_render_object(true), + mute_transition: true + } }; } - create_render_object(): CountdownRenderObject { + async create_render_object(proxy?: boolean): Promise { return { - backgroundImage: get_image_b64(this.item_props.BackgroundImage), + backgroundImage: await this.get_background_image(proxy), backgroundColor: this.item_props.BackgroundColor, slide: 0, slides: undefined, @@ -132,6 +141,15 @@ export default class Countdown extends SequenceItemBase { }; } + protected async get_background_image(proxy?: boolean): Promise { + // check wether the images have yet been laoded + if (this.props.backgroundImage === undefined) { + await this.load_backgroundImage(this.props.BackgroundImage.path); + } + + return this.props.backgroundImage[proxy ? "proxy" : "orig"]; + } + get active_slide(): number { // always return 0, because there is only 1 slide return 0; diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index 882473b..fb129f7 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -1,6 +1,7 @@ -import fs from "fs"; +import { promises as fs } from "fs"; import path from "path"; import mime from "mime-types"; +import sharp from "sharp"; import Config from "../config"; import Song, { ClientSongSlides, SongProps, SongRenderObject } from "./Song"; @@ -16,6 +17,10 @@ export interface ItemPropsBase { Color: string; Item: number; selectable: boolean; + backgroundImage?: { + orig: string; + proxy: string; + } } export type ItemProps = ItemPropsBase | SongProps | CountdownProps @@ -26,6 +31,7 @@ export interface ItemRenderObjectBase { slide: number; backgroundImage?: string; backgroundColor?: string; + mute_transition?: boolean; } export type ItemRenderObject = ItemRenderObjectBase | SongRenderObject | CountdownRenderObject; @@ -35,7 +41,7 @@ export interface ClientItemSlidesBase { title: string; item: number; slides: object; - slides_template: ItemRenderObject; + slides_template: ItemRenderObject & { mute_transition: true; }; } export type ClientItemSlides = ClientItemSlidesBase | ClientSongSlides | ClientCountdownSlides; @@ -53,8 +59,8 @@ export abstract class SequenceItemBase { protected abstract item_props: ItemProps; protected abstract SlideCount: number; - abstract create_render_object(slide?: number); - abstract create_client_object_item_slides(): ClientItemSlides; + abstract create_render_object(proxy?: boolean, slide?: number); + abstract create_client_object_item_slides(): Promise; abstract set_active_slide(slide?: number): number; /** @@ -84,15 +90,31 @@ export abstract class SequenceItemBase { return slide; } + protected async load_backgroundImage(image_path: string) { + let img_buffer; + + try { + img_buffer = await fs.readFile(path.join(Config.path.backgroundImage, image_path)); + } catch (e) { + this.item_props.backgroundImage = { + orig: "", + proxy: "" + }; + + return; + } + + const img_buffer_proxy = sharp(img_buffer).resize(240).toBuffer(); + + this.item_props.backgroundImage = { + orig: `data:${mime.lookup(image_path)};base64,` + (img_buffer).toString("base64"), + proxy: `data:${mime.lookup(image_path)};base64,` + (await img_buffer_proxy).toString("base64") + }; + } + get props(): ItemProps { return this.item_props; } -} -export function get_image_b64(image_path: string): string { - try { - return `data:${mime.lookup(image_path)};base64,` + fs.readFileSync(path.join(Config.path.backgroundImage, image_path)).toString("base64"); - } catch (e) { - return ""; - } + protected abstract get_background_image(proxy?: boolean): Promise; } \ No newline at end of file diff --git a/src/server/SequenceItems/Song.ts b/src/server/SequenceItems/Song.ts index a27a6f5..76efe18 100644 --- a/src/server/SequenceItems/Song.ts +++ b/src/server/SequenceItems/Song.ts @@ -1,4 +1,4 @@ -import { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase, get_image_b64 } from "./SequenceItem"; +import { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; import SongFile, { ItemPartClient, LyricPart, TitlePart } from "./SongFile"; import path from "path"; @@ -31,7 +31,7 @@ export interface SongRenderObject extends ItemRenderObjectBase { export interface ClientSongSlides extends ClientItemSlidesBase { Type: "Song" slides: ItemPartClient[]; - slides_template: SongRenderObject; + slides_template: SongRenderObject & { mute_transition: true; }; } export default class Song extends SequenceItemBase { @@ -91,7 +91,6 @@ export default class Song extends SequenceItemBase { } } } - } get_verse_order(): string[] { @@ -102,7 +101,7 @@ export default class Song extends SequenceItemBase { } } - create_render_object(slide?: number): SongRenderObject { + async create_render_object(proxy?: boolean, slide?: number): Promise { if (slide === undefined) { slide = this.active_slide; } @@ -115,7 +114,8 @@ export default class Song extends SequenceItemBase { this.SongFile.part_title ], slide, - languages: this.languages + languages: this.languages, + backgroundImage: await this.get_background_image(proxy) }; // add the individual parts to the output-object @@ -141,8 +141,6 @@ export default class Song extends SequenceItemBase { } } - return_object.backgroundImage = get_image_b64(this.SongFile.metadata.BackgroundImage); - return return_object; } @@ -184,7 +182,7 @@ export default class Song extends SequenceItemBase { return slide_steps; } - create_client_object_item_slides() { + async create_client_object_item_slides(): Promise { const return_item: ClientSongSlides = { Type: "Song", title: this.item_props.Caption, @@ -192,7 +190,10 @@ export default class Song extends SequenceItemBase { slides: [ this.SongFile.get_title_client() ], - slides_template: this.create_render_object(0) + slides_template: { + ...await this.create_render_object(true, 0), + mute_transition: true + } }; for (const part_name of this.get_verse_order()) { @@ -215,6 +216,15 @@ export default class Song extends SequenceItemBase { return return_item; } + protected async get_background_image(proxy?: boolean): Promise { + // check wether the images have yet been laoded + if (this.props.backgroundImage === undefined) { + await this.load_backgroundImage(this.SongFile.metadata.BackgroundImage); + } + + return this.props.backgroundImage[proxy ? "proxy" : "orig"]; + } + get active_slide(): number { return this.active_slide_number; } diff --git a/src/server/control.ts b/src/server/control.ts index e6aa0e6..552171e 100644 --- a/src/server/control.ts +++ b/src/server/control.ts @@ -85,13 +85,13 @@ class Control { * @param clientID * @param ws */ - private get_item_slides(item: number, clientID?: string, ws?: WebSocket) { + private async get_item_slides(item: number, clientID?: string, ws?: WebSocket) { // type-check the item if (typeof item === "number") { const message: JGCPSend.ItemSlides = { command: "item-slides", clientID, - ...this.sequence.create_client_object_item_slides(item) + ...await this.sequence.create_client_object_item_slides(item) }; ws?.send(JSON.stringify(message)); diff --git a/yarn.lock b/yarn.lock index 99aa13c..b742e5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,13 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@emnapi/runtime@^0.45.0": + version "0.45.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-0.45.0.tgz#e754de04c683263f34fd0c7f32adfe718bbe4ddd" + integrity sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w== + dependencies: + tslib "^2.4.0" + "@esbuild/aix-ppc64@0.20.0": version "0.20.0" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" @@ -173,6 +180,119 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== +"@img/sharp-darwin-arm64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz#0a52a82c2169112794dac2c71bfba9e90f7c5bd1" + integrity sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.0.1" + +"@img/sharp-darwin-x64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz#982e26bb9d38a81f75915c4032539aed621d1c21" + integrity sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.0.1" + +"@img/sharp-libvips-darwin-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz#81e83ffc2c497b3100e2f253766490f8fad479cd" + integrity sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw== + +"@img/sharp-libvips-darwin-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz#fc1fcd9d78a178819eefe2c1a1662067a83ab1d6" + integrity sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog== + +"@img/sharp-libvips-linux-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz#26eb8c556a9b0db95f343fc444abc3effb67ebcf" + integrity sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA== + +"@img/sharp-libvips-linux-arm@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz#2a377b959ff7dd6528deee486c25461296a4fa8b" + integrity sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ== + +"@img/sharp-libvips-linux-s390x@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz#af28ac9ba929204467ecdf843330d791e9421e10" + integrity sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ== + +"@img/sharp-libvips-linux-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz#4273d182aa51912e655e1214ea47983d7c1f7f8d" + integrity sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw== + +"@img/sharp-libvips-linuxmusl-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz#d150c92151cea2e8d120ad168b9c358d09c77ce8" + integrity sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg== + +"@img/sharp-libvips-linuxmusl-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz#e297c1a4252c670d93b0f9e51fca40a7a5b6acfd" + integrity sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw== + +"@img/sharp-linux-arm64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz#af3409f801a9bee1d11d0c7e971dcd6180f80022" + integrity sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.0.1" + +"@img/sharp-linux-arm@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz#181f7466e6ac074042a38bfb679eb82505e17083" + integrity sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.0.1" + +"@img/sharp-linux-s390x@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz#9c171f49211f96fba84410b3e237b301286fa00f" + integrity sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.0.1" + +"@img/sharp-linux-x64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz#b956dfc092adc58c2bf0fae2077e6f01a8b2d5d7" + integrity sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.0.1" + +"@img/sharp-linuxmusl-arm64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz#10e0ec5a79d1234c6a71df44c9f3b0bef0bc0f15" + integrity sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.0.1" + +"@img/sharp-linuxmusl-x64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz#29e0030c24aa27c38201b1fc84e3d172899fcbe0" + integrity sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.0.1" + +"@img/sharp-wasm32@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz#38d7c740a22de83a60ad1e6bcfce17462b0d4230" + integrity sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ== + dependencies: + "@emnapi/runtime" "^0.45.0" + +"@img/sharp-win32-ia32@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz#09456314e223f68e5417c283b45c399635c16202" + integrity sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g== + +"@img/sharp-win32-x64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz#148e96dfd6e68747da41a311b9ee4559bb1b1471" + integrity sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -491,11 +611,27 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + commander@^9.4.0: version "9.5.0" resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" @@ -527,6 +663,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +detect-libc@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" + integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -842,6 +983,11 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -1149,6 +1295,35 @@ serialport@10.5.0: "@serialport/stream" "10.5.0" debug "^4.3.3" +sharp@^0.33.2: + version "0.33.2" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.2.tgz#fcd52f2c70effa8a02160b1bfd989a3de55f2dfb" + integrity sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ== + dependencies: + color "^4.2.3" + detect-libc "^2.0.2" + semver "^7.5.4" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.33.2" + "@img/sharp-darwin-x64" "0.33.2" + "@img/sharp-libvips-darwin-arm64" "1.0.1" + "@img/sharp-libvips-darwin-x64" "1.0.1" + "@img/sharp-libvips-linux-arm" "1.0.1" + "@img/sharp-libvips-linux-arm64" "1.0.1" + "@img/sharp-libvips-linux-s390x" "1.0.1" + "@img/sharp-libvips-linux-x64" "1.0.1" + "@img/sharp-libvips-linuxmusl-arm64" "1.0.1" + "@img/sharp-libvips-linuxmusl-x64" "1.0.1" + "@img/sharp-linux-arm" "0.33.2" + "@img/sharp-linux-arm64" "0.33.2" + "@img/sharp-linux-s390x" "0.33.2" + "@img/sharp-linux-x64" "0.33.2" + "@img/sharp-linuxmusl-arm64" "0.33.2" + "@img/sharp-linuxmusl-x64" "0.33.2" + "@img/sharp-wasm32" "0.33.2" + "@img/sharp-win32-ia32" "0.33.2" + "@img/sharp-win32-x64" "0.33.2" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -1161,6 +1336,13 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -1207,7 +1389,7 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== -tslib@^2.5.0: +tslib@^2.4.0, tslib@^2.5.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== From 843a75c9ee021e8f8c35d99b6717cf26cbf212fb Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Thu, 22 Feb 2024 15:14:09 +0100 Subject: [PATCH 07/16] message-box lead to typescript-client lead to snake_case lead to typescript-client --- .eslintrc | 21 ++- .gitignore | 4 +- .vscode/tasks.json | 32 +++- README.md | 4 +- build.ps1 | 4 +- casparcg-templates/JohnCG/.eslintrc | 10 -- casparcg-templates/JohnCG/Countdown.html | 181 +------------------ casparcg-templates/JohnCG/Song.html | 188 +------------------- client/main.css | 76 ++++++++ client/main.html | 7 +- config.json | 12 +- package.json | 13 +- client/main.js => src/client/main.ts | 210 ++++++++++++---------- src/client/message_box.ts | 126 ++++++++++++++ src/client/off_main.d.ts | 9 + src/server/JGCPReceiveMessages.ts | 8 +- src/server/JGCPSendMessages.ts | 32 +++- src/server/Sequence.ts | 65 ++++--- src/server/SequenceItems/Comment.ts | 33 +++- src/server/SequenceItems/Countdown.ts | 136 ++++++++------- src/server/SequenceItems/SequenceItem.ts | 95 +++++++--- src/server/SequenceItems/Song.ts | 58 ++++--- src/server/SequenceItems/SongFile.ts | 23 ++- src/server/config.ts | 14 +- src/server/control.ts | 59 ++++--- src/server/main.ts | 13 +- src/server/servers/http-server.ts | 10 +- src/server/servers/osc-server.ts | 24 +-- src/server/servers/websocket-server.ts | 3 +- src/templates/Countdown.ts | 212 +++++++++++++++++++++++ src/templates/Song.ts | 210 ++++++++++++++++++++++ tsconfig_client.json | 13 ++ tsconfig.json => tsconfig_server.json | 0 tsconfig_templates.json | 12 ++ 34 files changed, 1196 insertions(+), 721 deletions(-) delete mode 100644 casparcg-templates/JohnCG/.eslintrc rename client/main.js => src/client/main.ts (58%) create mode 100644 src/client/message_box.ts create mode 100644 src/client/off_main.d.ts create mode 100644 src/templates/Countdown.ts create mode 100644 src/templates/Song.ts create mode 100644 tsconfig_client.json rename tsconfig.json => tsconfig_server.json (100%) create mode 100644 tsconfig_templates.json diff --git a/.eslintrc b/.eslintrc index 32262c8..9475adf 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,6 +13,25 @@ "semi": ["error", "always"], "object-curly-spacing": ["error", "always"], "quotes": ["error"], - "quote-props": ["error", "as-needed"] + "quote-props": ["error", "as-needed"], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "default", + "format": ["snake_case"] + }, + { + "selector": "typeLike", + "format": ["PascalCase"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + }, + { + "selector": "import", + "format": ["PascalCase", "snake_case", "camelCase"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + } + ] } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 733481e..b4b5ed6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules out -dist \ No newline at end of file +dist +client/*.js +casparcg-templates/JohnCG/*.js \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 143fecf..f3b4fdf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,23 +3,39 @@ "tasks": [ { "type": "npm", - "script": "build", - "group": { - "kind": "build", - "isDefault": true - }, + "script": "build-release", + "group": "build", "problemMatcher": [], - "label": "npm: build", - "detail": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./build.ps1" + "label": "build release" }, { "type": "typescript", - "tsconfig": "tsconfig.json", + "tsconfig": "tsconfig_server.json", "problemMatcher": [ "$tsc" ], "group": "build", "label": "tsc: build - server" + }, + { + "type": "typescript", + "tsconfig": "tsconfig_client.json", + "option": "watch", + "problemMatcher": [ + "$tsc-watch" + ], + "group": "build", + "label": "tsc: watch - tsconfig_client.json" + }, + { + "type": "typescript", + "tsconfig": "tsconfig_templates.json", + "option": "watch", + "problemMatcher": [ + "$tsc-watch" + ], + "group": "build", + "label": "tsc: watch - tsconfig_templates.json" } ] } \ No newline at end of file diff --git a/README.md b/README.md index 0a97254..cc740d8 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,6 @@ Generate lyric-graphics and play them out through CasparCG. - CasparCG: split text and image in two layers: enables text without background - rewrite client in typescript - create dummy-sequence-items for unsupported ones -- disable buttons, when no sequence is loaded \ No newline at end of file +- disable buttons, when no sequence is loaded +- standardize all interfaces to snake_case +- implement all countdown modes diff --git a/build.ps1 b/build.ps1 index 87ece63..2d75962 100644 --- a/build.ps1 +++ b/build.ps1 @@ -8,7 +8,9 @@ New-Item -Type Directory .\Dist\build New-Item -Type Directory .\Dist\$build_name # bundle the files -yarn esbuild src/server/main.ts --bundle --platform=node --outfile=dist/build/main.js +yarn run build-server +yarn run build-client +yarn run build-templates # create sea-prep.blob node --experimental-sea-config .\sea-config.json diff --git a/casparcg-templates/JohnCG/.eslintrc b/casparcg-templates/JohnCG/.eslintrc deleted file mode 100644 index 123c73b..0000000 --- a/casparcg-templates/JohnCG/.eslintrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "root": true, - "env": { - "browser": true - }, - "parserOptions": { - "ecmaVersion": 2018 - }, - "extends": "eslint:recommended" -} \ No newline at end of file diff --git a/casparcg-templates/JohnCG/Countdown.html b/casparcg-templates/JohnCG/Countdown.html index e7f229e..27dfeec 100644 --- a/casparcg-templates/JohnCG/Countdown.html +++ b/casparcg-templates/JohnCG/Countdown.html @@ -72,184 +72,5 @@ - + \ No newline at end of file diff --git a/casparcg-templates/JohnCG/Song.html b/casparcg-templates/JohnCG/Song.html index 04fb604..0dbdd59 100644 --- a/casparcg-templates/JohnCG/Song.html +++ b/casparcg-templates/JohnCG/Song.html @@ -184,191 +184,5 @@
- + \ No newline at end of file diff --git a/client/main.css b/client/main.css index 176629c..ec6e4ef 100644 --- a/client/main.css +++ b/client/main.css @@ -290,3 +290,79 @@ div.button, div.button > * { top: 0; left: 0; } + +#error_container { + position: absolute; + top: 1rem; + right: 1rem; + + width: 32rem; +} + +.message_box { + background-color: white; + color: black; + + display: flex; + align-items: center; + + border-radius: 0.25rem; + + max-height: 0; + + cursor: pointer; +} + +.message_box.visible { + max-height: 100vh; + transition: max-height 1s ease-in; + + margin-bottom: 0.25rem; + + opacity: 1; +} + +.message_box.fade_out { + opacity: 0; + + transition: opacity 1s ease-out; +} + +.message_box.visible:first-child { + margin-block-start: 0; +} + +.message_box > .symbol { + margin: 0.25rem; + + display: flex; + align-items: center; + flex-direction: column; + aspect-ratio: 1; + height: 1em; +} + +.message_box > .symbol > i.error { + color: red; +} + +.message_box > .symbol > i.log { + color: black; +} + +.message_box > .symbol > i.debug { + color: lightblue; +} + +.message_box > div { + display: inline-block; +} + +.message_box > .text { + flex: 1; +} + +.message_box > .time { + margin-left: auto; + margin-right: 0.5rem; +} diff --git a/client/main.html b/client/main.html index 65c99ea..5a453d7 100644 --- a/client/main.html +++ b/client/main.html @@ -36,6 +36,10 @@
+
+
+ +
@@ -46,6 +50,7 @@
- +
+ \ No newline at end of file diff --git a/config.json b/config.json index ee9e03c..bfa1fde 100644 --- a/config.json +++ b/config.json @@ -1,10 +1,10 @@ { "behaviour": { - "showOnLoad": true + "show_on_load": true }, "path": { - "backgroundImage": "C:/path/to/image/directory", - "song": "D:/path/to/song/directory" + "background_image": "C:/path/to/image/directory", + "song": "D:/Coding_z1glr/liedtexte_git" }, "casparcg": { "templates": { @@ -23,7 +23,7 @@ } ] }, - "clientServer": { + "client_server": { "http": { "port": 8888 }, @@ -31,11 +31,11 @@ "port": 8765 } }, - "oscServer": { + "osc_server": { "port": 8766 }, "companion": { "address": "localhost", - "oscPort": 12321 + "osc_port": 12321 } } diff --git a/package.json b/package.json index 3aa798b..5ec62d6 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,15 @@ "typescript": "^5.3.3" }, "scripts": { - "lint": "eslint --ext .ts", - "precommit": "eslint . --ext .ts", - "build": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./build.ps1" + "build-release": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./build.ps1", + + "build-server": "esbuild src/server/main.ts --tsconfig=tsconfig_server.json --bundle --platform=node --outfile=dist/build/main.js", + + "build-client": "esbuild src/client/main.ts --tsconfig=tsconfig_client.json --bundle --outfile=client/main.js", + "watch-client": "esbuild src/client/main.ts --tsconfig=tsconfig_client.json --bundle --watch --outfile=client/main.js", + + "build-templates": "esbuild src/templates/*.ts --target=chrome71 --tsconfig=tsconfig_templates.json --outdir=casparcg-templates/JohnCG/", + "watch-templates": "esbuild src/templates/*.ts --target=chrome71 --tsconfig=tsconfig_templates.json --watch --outdir=casparcg-templates/JohnCG/" + } } diff --git a/client/main.js b/src/client/main.ts similarity index 58% rename from client/main.js rename to src/client/main.ts index 1519649..1049fb7 100644 --- a/client/main.js +++ b/src/client/main.ts @@ -1,67 +1,79 @@ +import MessageLog from "./message_box.js"; + +import * as JGCPSend from "../server/JGCPSendMessages"; +import { ItemPartClient } from "../server/SequenceItems/SongFile"; +import { ClientItemSlides } from "../server/SequenceItems/SequenceItem"; +import { ClientSequenceItems } from "../server/Sequence"; +import { CountdownProps } from "../server/SequenceItems/Countdown"; + const config = { websocket: { port: 8765 } }; +const msg_log = new MessageLog(document.querySelector("#error_container")!); + function open_sequence(e) { - let file = e.target.files[0]; - let reader = new FileReader(); + const file = e.target.files[0]; + const reader = new FileReader(); reader.onload = function(e) { - let rawData = e.target.result; + const raw_data = e.target?.result; ws.send(JSON.stringify({ - command: "open-sequence", - sequence: rawData + command: "open_sequence", + sequence: raw_data })); - } + }; reader.readAsText(file); } -document.querySelector("#input_open_sequence").addEventListener("change", open_sequence) +document.querySelector("#input_open_sequence")?.addEventListener("change", open_sequence); function button_navigate(type, steps) { ws.send(JSON.stringify({ command: "navigate", type, steps, - clientID + client_id: client_id })); } -document.querySelector("#navigate_item_prev").addEventListener("click", () => { button_navigate("item", -1) }); -document.querySelector("#navigate_item_next").addEventListener("click", () => { button_navigate("item", 1) }); -document.querySelector("#navigate_slide_prev").addEventListener("click", () => { button_navigate("slide", -1) }); -document.querySelector("#navigate_slide_next").addEventListener("click", () => { button_navigate("slide", 1) }); +document.querySelector("#navigate_item_prev")?.addEventListener("click", () => { button_navigate("item", -1); }); +document.querySelector("#navigate_item_next")?.addEventListener("click", () => { button_navigate("item", 1); }); +document.querySelector("#navigate_slide_prev")?.addEventListener("click", () => { button_navigate("slide", -1); }); +document.querySelector("#navigate_slide_next")?.addEventListener("click", () => { button_navigate("slide", 1); }); function button_visibility(visibility) { ws.send(JSON.stringify({ - command: "set-visibility", + command: "set_visibility", visibility, - clientID + client_id: client_id })); } -document.querySelector("#set_visibility_hide").addEventListener("click", () => { button_visibility(false) }); -document.querySelector("#set_visibility_show").addEventListener("click", () => { button_visibility(true) }); +document.querySelector("#set_visibility_hide")?.addEventListener("click", () => { button_visibility(false); }); +document.querySelector("#set_visibility_show")?.addEventListener("click", () => { button_visibility(true); }); + +document.querySelector("#show_error_log")?.addEventListener("click", () => msg_log.error("foobar")); -function display_items(data) { +function display_items(data: ClientSequenceItems) { const div_sequence_items = document.querySelector("#sequence_items"); // initialize init(); - for (let item of data.sequence_items) { + for (const item of data.sequence_items) { const div_sequence_item_container = document.createElement("div"); div_sequence_item_container.classList.add("sequence_item_container"); - div_sequence_item_container.dataset.item_number = item.Item; + div_sequence_item_container.dataset.item_number = item.item.toString(); - // if the item is selectable, give it the class and add the onclic-event + // if the item is selectable, give it the class and add the onclick-event if (item.selectable) { div_sequence_item_container.classList.add("selectable"); - div_sequence_item_container.onclick = function() { + div_sequence_item_container.addEventListener("click", function() { request_item_slides(Number(this.dataset.item_number)); - }; + }); } const div_sequence_item_color_indicator = document.createElement("div"); @@ -74,18 +86,20 @@ function display_items(data) { div_sequence_item.classList.add("sequence_item"); // if it's a Countdown-Object, insert the time - if (item.Type === "Countdown") { - div_sequence_item.innerText = item.Caption.replace("%s", item.Time); + if (item.type === "Countdown") { + div_sequence_item.innerText = item.Caption.replace("%s", (item as CountdownProps).Time); } else { div_sequence_item.innerText = item.Caption; } div_sequence_item_container.append(div_sequence_item); - div_sequence_items.append(div_sequence_item_container); + div_sequence_items?.append(div_sequence_item_container); } // display the visibility state - display_visibility_state(data.visibility); + display_visibility_state(data.metadata.visibility); + + msg_log.log("Sequence is loaded"); } function request_item_slides(item) { @@ -93,59 +107,64 @@ function request_item_slides(item) { selected_item_number = item; // clear the current slides - document.querySelector("#slides_view_container").innerHTML = ""; + const slides_view_container = document.querySelector("#slides_view_container"); + if (slides_view_container !== null) { + slides_view_container.innerHTML = ""; + } // clear the selected item document.querySelector(".sequence_item_container.selected")?.classList.remove("selected"); ws.send(JSON.stringify({ - command: "request-item-slides", + command: "request_item_slides", item: item, - clientID + client_id: client_id })); } } -function display_item_slides(data) { +function display_item_slides(data: JGCPSend.ItemSlides) { const div_slides_view_container = document.querySelector("#slides_view_container"); // select the sequence-item select_item(data.item); // create the individual arrays for parallel processing - let part_arrays = []; + let part_arrays: HTMLDivElement[] = []; - switch (data.Type) { + switch (data.type) { case "Song": - part_arrays = create_song_slides(data); + part_arrays = create_song_slides(data as JGCPSend.SongSlides); break; case "Countdown": - part_arrays = create_countdown_slides(data); + part_arrays = create_countdown_slides(data as JGCPSend.CountdownSlides); break; default: - console.error(`'${data.Type}' is not supported`) + console.error(`'${data.type}' is not supported`); } part_arrays.forEach((slide_part) => { - div_slides_view_container.append(slide_part); + div_slides_view_container?.append(slide_part); }); - set_active_slide(data.clientID === clientID); + set_active_slide(data.client_id === client_id); + + msg_log.log("Item is loaded"); } -function create_song_slides(data) { - let slide_counter = 0; +function create_song_slides(data: JGCPSend.SongSlides): HTMLDivElement[] { + let slide_counter: number = 0; // create the individual arrays for parallel processing - let part_arrays = []; + const part_arrays_prototype: [number, ItemPartClient][] = []; data.slides.forEach((part) => { - part_arrays.push([slide_counter, part]); + part_arrays_prototype.push([slide_counter, part]); slide_counter += part.slides; }); - part_arrays = part_arrays.map(([slides_start, part]) => { + const part_arrays = part_arrays_prototype.map(([slides_start, part]) => { // create the container for the part const div_slide_part = document.createElement("div"); div_slide_part.classList.add("slide_part"); @@ -169,9 +188,9 @@ function create_song_slides(data) { } break; } - let object_iter_array = [...Array(part.slides).keys()]; + const object_iter_array_proto = [...Array(part.slides).keys()]; - object_iter_array = object_iter_array.map((ii) => { + const object_iter_array = object_iter_array_proto.map((ii) => { return { index: ii, slide: create_slide_object(data, slides_start + ii) @@ -188,7 +207,7 @@ function create_song_slides(data) { return part_arrays; } -function create_countdown_slides(data) { +function create_countdown_slides(data: JGCPSend.CountdownSlides) { // create the container for the part const div_slide_part = document.createElement("div"); div_slide_part.classList.add("slide_part"); @@ -212,18 +231,18 @@ function create_countdown_slides(data) { return [div_slide_part]; } -function create_slide_object(data, number) { +function create_slide_object(data: ClientItemSlides, number: number) { const div_slide_container = document.createElement("div"); div_slide_container.classList.add("slide_container"); const slide_object = document.createElement("object"); - switch (data.Type) { + switch (data.type) { case "Song": slide_object.data = "Templates/Song.html"; break; case "Countdown": - slide_object.data = "Templates/Countdown.html" + slide_object.data = "Templates/Countdown.html"; break; } @@ -231,32 +250,32 @@ function create_slide_object(data, number) { div_slide_container.append(slide_object); - slide_object.dataset.slide_number = number; + slide_object.dataset.slide_number = number.toString(); slide_object.addEventListener("load", () => { - slide_object.contentWindow.update(JSON.stringify(data.slides_template)); + slide_object.contentWindow?.update(JSON.stringify(data.slides_template)); - switch (data.Type) { + switch (data.type) { case "Song": - slide_object.contentWindow.jump(number); + slide_object.contentWindow?.jump(number.toString()); break; } - slide_object.contentWindow.play(); + slide_object.contentWindow?.play(); // register click event - slide_object.contentWindow.addEventListener("click", () => { + slide_object.contentWindow?.addEventListener("click", () => { request_item_slide_select( - Number(document.querySelector(".sequence_item_container.selected").dataset.item_number), + Number(document.querySelector("div.sequence_item_container.selected")?.dataset.item_number), number ); - }) + }); }); return div_slide_container; } -function select_item(item) { +function select_item(item: number) { // store the selected item selected_item_number = item; @@ -268,19 +287,19 @@ function select_item(item) { // add the selected class to the current item const selected_sequence_item = document.querySelector(`[data-item_number='${selected_item_number}']`); - selected_sequence_item.classList.add("selected"); + selected_sequence_item?.classList.add("selected"); } -function request_item_slide_select(item, slide) { +function request_item_slide_select(item: number, slide: number) { ws.send(JSON.stringify({ - command: "select-item-slide", + command: "select_item_slide", item: item, slide: slide, - clientID + client_id: client_id })); } -function set_active_slide(scroll = false) { +function set_active_slide(scroll: boolean = false) { // deselect the previous active slide const selected_slide = document.querySelector(".slide.active"); if (selected_slide !== null) { @@ -291,10 +310,10 @@ function set_active_slide(scroll = false) { selected_header.classList.remove("active"); } - const selected_item = document.querySelector(".sequence_item_container.selected"); + const selected_item = document.querySelector("div.sequence_item_container.selected"); if (selected_item !== null) { // if the currently displayed and selected sequence item is the active one, select the active slide - if (selected_item.dataset.item_number == active_item_slide.item) { + if (Number(selected_item.dataset.item_number) === active_item_slide.item) { // remove the selected class from the previous item const prev_selected_item_slide = document.querySelector(".slide.active"); if (prev_selected_item_slide !== null) { @@ -303,31 +322,31 @@ function set_active_slide(scroll = false) { // add the selected class to the current slide const selected_item_slide = document.querySelector(`[data-slide_number='${active_item_slide.slide}']`); - selected_item_slide.classList.add("active"); + selected_item_slide?.classList.add("active"); - selected_item_slide.parentElement.parentElement.parentElement.querySelector(".header").classList.add("active"); + selected_item_slide?.parentElement?.parentElement?.parentElement?.querySelector(".header")?.classList.add("active"); // if we requested this, scroll there if (scroll) { - selected_item_slide.parentElement.scrollIntoView({ behavior: "smooth", block: "nearest"}); + selected_item_slide?.parentElement?.scrollIntoView({ behavior: "smooth", block: "nearest" }); } } } } -function set_active_item_slide(data, message_clientID) { +function set_active_item_slide(data, message_client_id) { // store the data active_item_slide = data; // decide wether to jump there - const jump = message_clientID === clientID || message_clientID === undefined; + const jump = message_client_id === client_id || message_client_id === undefined; // remove the "active" class from the previous sequence-item and add it to the new one const prev_selected_item = document.querySelector(".sequence_item_container.active"); if (prev_selected_item !== null) { prev_selected_item.classList.remove("active"); } - document.querySelector(`.sequence_item_container[data-item_number='${active_item_slide.item}']`).classList.add("active"); + document.querySelector(`div.sequence_item_container[data-item_number='${active_item_slide.item}']`)?.classList.add("active"); if (jump && active_item_slide.item !== selected_item_number) { request_item_slides(active_item_slide.item); @@ -341,17 +360,17 @@ function display_visibility_state(state) { const button_show = document.querySelector("#set_visibility_show"); if (state) { - button_hide.classList.remove("active"); - button_show.classList.add("active"); + button_hide?.classList.remove("active"); + button_show?.classList.add("active"); } else { - button_hide.classList.add("active"); - button_show.classList.remove("active"); + button_hide?.classList.add("active"); + button_show?.classList.remove("active"); } } function display_state_change(data) { const key_map = { - activeItemSlide: set_active_item_slide, + active_item_slide: set_active_item_slide, visibility: display_visibility_state }; @@ -367,23 +386,32 @@ function random_4_hex() { } function blank_screen(state) { - document.querySelector("#blank").style.display = state ? "unset" : "none"; + const blank_div = document.querySelector("div#blank"); + if (blank_div !== null) { + blank_div.style.display = state ? "unset" : "none"; + } } function init() { selected_item_number = null; - // remove all sequence-items - document.querySelector("#sequence_items").innerHTML = ""; + // remove all sequence_items + const sequence_items = document.querySelector("#sequence_items"); + if (sequence_items !== null) { + sequence_items.innerHTML = ""; + } // remove all item-slides - document.querySelector("#slides_view_container").innerHTML = ""; + const slides_view_container = document.querySelector("#slides_view_container"); + if (slides_view_container !== null) { + slides_view_container.innerHTML = ""; + } // remove the visibility-selection document.querySelector("[id^=set_visibility_].active")?.classList.remove("active"); } -const clientID = `${random_4_hex()}-${random_4_hex()}-${random_4_hex()}-${random_4_hex()}`; +const client_id = `${random_4_hex()}-${random_4_hex()}-${random_4_hex()}-${random_4_hex()}`; let selected_item_number; function ws_connect() { @@ -400,19 +428,23 @@ function ws_connect() { ws.addEventListener("message", (event) => { const data = JSON.parse(event.data); + + + console.dir(data); + const command_parser_map = { - "sequence-items": display_items, - "item-slides": display_item_slides, + sequence_items: display_items, + item_slides: display_item_slides, state: display_state_change, clear: init, response: (response) => { switch (Number(response.code.toString()[0])) { case 4: - console.error(response); + msg_log.error(response.message); break; default: - console.debug(response); + msg_log.debug(response.message); } } }; @@ -425,15 +457,13 @@ function ws_connect() { }); ws.addEventListener("error", (event) => { - console.error(`Server connection encountered error '${event.message}'. Closing socket`); + msg_log.error(`Server connection encountered error '${event.message}'. Closing socket`); ws.close(); }); - - ws.addEventListener("close", async () => { - console.error("No connection to server. Retrying in 1s"); + console.log("No connection to server. Retrying in 1s"); // blank the screen because there is no connection blank_screen(true); @@ -442,7 +472,7 @@ function ws_connect() { ws_connect(); }, 1000); - }) + }); } let ws; @@ -451,4 +481,4 @@ ws_connect(); let active_item_slide = { item: 0, slide: 0 -}; \ No newline at end of file +}; diff --git a/src/client/message_box.ts b/src/client/message_box.ts new file mode 100644 index 0000000..cff9cea --- /dev/null +++ b/src/client/message_box.ts @@ -0,0 +1,126 @@ +type MessageType = "error" | "log" | "debug"; + +interface Message { + type: MessageType; + time: Date; + text: string; +} + +const icon_map = { + error: "fa-solid fa-xmark error", + log: "fa-solid fa-info log", + debug: "fa-solid fa-bug debug" +}; + +export default class MessageLog { + private messages: Message[] = []; + + private message_box_container: HTMLElement; + + private settings = { + show: { + error: true, + log: true, + debug: false + } + }; + + constructor(message_box_container: HTMLElement) { + this.message_box_container = message_box_container; + } + + error(text: string) { + this.add_message(text, "error"); + } + + log(text: string) { + this.add_message(text, "log"); + } + + debug(text: string) { + this.add_message(text, "debug"); + } + + private add_message(text: string, type: MessageType) { + const message: Message = { + text, + type, + time: new Date() + }; + + this.messages.push(message); + + if (this.settings.show[type]) { + this.create_msg_box(message); + } + } + + private create_msg_box(message: Message) { + // div for the individual elements of the box + const message_box = document.createElement("div"); + message_box.classList.add("message_box"); + + // container for the symbol + const symbol_container = document.createElement("div"); + symbol_container.classList.add("symbol"); + message_box.append(symbol_container); + + // symbol of the message + const symbol = document.createElement("i"); + symbol.classList.add(...icon_map[message.type].split(" ")); + symbol_container.append(symbol); + + // message-text + const error_text = document.createElement("div"); + error_text.classList.add("text"); + error_text.innerText = message.text; + message_box.append(error_text); + + // time of the error + const error_time = document.createElement("div"); + error_time.classList.add("time"); + error_time.innerText = message.time.toLocaleTimeString(); + message_box.append(error_time); + + // display the message-box + this.message_box_container.prepend(message_box); + + // function to transition out and remove itself + const self_remove = (fade_out_time?: string) => { + // if there is a fade-out-time specified, use it + if (fade_out_time !== undefined) { + // if it is instant, remove it instantly, since the "transitionend"-event will never fire + if (fade_out_time === "0s") { + message_box.remove(); + + // return since the box is removed + return; + } else { + // set the given transition duration + message_box.style.transitionDuration = fade_out_time; + } + } + + // add the class for the fade-out + message_box.classList.add("fade_out"); + + // eventlistener to remove the end after the transition + message_box.addEventListener("transitionend", () => { + message_box.remove(); + }); + }; + + // if the error_message gets clicked, remove it, but faster + message_box.addEventListener("click", () => self_remove("0.1s")); + + // delay the showing of the messagebox by a little bit to trigger the in-transition + setTimeout(() => { + message_box.classList.add("visible"); + }, 10); + + // auto remove the message after 5 seconds + setTimeout(() => { + self_remove(); + }, 5000); + } +} diff --git a/src/client/off_main.d.ts b/src/client/off_main.d.ts new file mode 100644 index 0000000..1d6cd1c --- /dev/null +++ b/src/client/off_main.d.ts @@ -0,0 +1,9 @@ +export {}; + +declare global { + interface Window { + update: (data: string) => void; + jump: (data: string) => void; + play: () => void; + } +} \ No newline at end of file diff --git a/src/server/JGCPReceiveMessages.ts b/src/server/JGCPReceiveMessages.ts index 6beea6a..d2707b6 100644 --- a/src/server/JGCPReceiveMessages.ts +++ b/src/server/JGCPReceiveMessages.ts @@ -2,7 +2,7 @@ * Base interface for Received JGCP-messages */ interface Base { - clientID?: string; + client_id?: string; } /** @@ -30,10 +30,10 @@ export interface ItemSlideSelect extends Base { /** * The different navigation-types */ -const _item_navigate_type = ["item", "slide"] as const; -export type NavigateType = (typeof _item_navigate_type)[number]; +const item_navigate_type = ["item", "slide"] as const; +export type NavigateType = (typeof item_navigate_type)[number]; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isItemNavigateType = (x: any): x is NavigateType => _item_navigate_type.includes(x); +export const is_item_navigate_type = (x: any): x is NavigateType => item_navigate_type.includes(x); export interface Navigate extends Base { command: "navigate"; diff --git a/src/server/JGCPSendMessages.ts b/src/server/JGCPSendMessages.ts index 87ff026..fcf50b8 100644 --- a/src/server/JGCPSendMessages.ts +++ b/src/server/JGCPSendMessages.ts @@ -1,11 +1,12 @@ import * as SequenceClass from "../server/Sequence"; -import { ClientItemSlidesBase } from "./SequenceItems/SequenceItem"; +import { ClientCountdownSlides } from "../server/SequenceItems/Countdown"; +import { ClientSongSlides } from "../server/SequenceItems/Song"; /** * Base interface for sent JGCP-messages */ interface Base { - clientID?: string; + client_id?: string; } /** @@ -18,10 +19,10 @@ export interface Response { } /** - * JGCP-messages with the sequence-items + * JGCP-messages with the sequence_items */ export interface Sequence extends Base, SequenceClass.ClientSequenceItems { - command: "sequence-items"; + command: "sequence_items"; } /** @@ -29,15 +30,30 @@ export interface Sequence extends Base, SequenceClass.ClientSequenceItems { */ export interface State extends Base { command: "state"; - activeItemSlide?: SequenceClass.ActiveItemSlide, + active_item_slide?: SequenceClass.ActiveItemSlide, visibility?: boolean; } -export interface ItemSlides extends Base, ClientItemSlidesBase { - clientID: string; - command: "item-slides"; +// export interface ItemSlides extends Base, ClientItemSlidesBase { +// clientID: string; +// command: "item-slides"; +// slides: string[][]; +// } + +interface ItemSlidesBase extends Base{ + client_id: string; + command: "item_slides"; } +export type SongSlides = ItemSlidesBase & ClientSongSlides; + +export type CountdownSlides = ItemSlidesBase & ClientCountdownSlides; + +// temporary until full feature set +export type NotImplementedSlides = ItemSlidesBase & { type: string; item: number; }; + +export type ItemSlides = SongSlides | CountdownSlides | NotImplementedSlides; + export interface Clear extends Base { command: "clear"; } diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index 540f140..febd788 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -2,7 +2,7 @@ import path from "path"; import { CasparCG } from "casparcg-connection"; import { SongElement } from "./SequenceItems/SongFile"; -import { ClientItemSlides, ItemProps, SequenceItem } from "./SequenceItems/SequenceItem"; +import { ClientItemSlides, ItemProps, ItemPropsBase, SequenceItem } from "./SequenceItems/SequenceItem"; import Song, { SongProps } from "./SequenceItems/Song"; import * as JGCPSend from "./JGCPSendMessages"; @@ -30,7 +30,7 @@ class Sequence { private active_item_number: number = 0; - private casparcg_visibility: boolean = Config.behaviour.showOnLoad; + private casparcg_visibility: boolean = Config.behaviour.show_on_load; readonly casparcg_connections: CasparCG[] = []; @@ -41,6 +41,7 @@ class Sequence { Config.casparcg.connections.forEach((connection_setting, index) => { const casparcg_connection = new CasparCG({ ...connection_setting, + // eslint-disable-next-line @typescript-eslint/naming-convention autoConnect: true }); @@ -48,9 +49,6 @@ class Sequence { casparcg_connection.addListener("connect", () => { // load the active-item this.casparcg_load_item(this.active_item_number, index, false); - - // remove the connect-listener again - casparcg_connection.removeAllListeners("connect"); }); // add the connection to the stored connections @@ -120,16 +118,18 @@ class Sequence { re_scan_sequence_item.lastIndex = 0; // store the regex results - let re_results_item: RegExpExecArray; + let re_results_item: RegExpExecArray | null; // store the data of the object - let item_data: ItemProps = { - Type: null, + let item_data: ItemPropsBase = { + /* eslint-disable @typescript-eslint/naming-convention */ + type: "", Caption: "", Color: "", - SlideCount: 0, - Item: this.sequence_items.length, + slide_count: 0, + item: this.sequence_items.length, selectable: true + /* eslint-enable @typescript-eslint/naming-convention */ }; // exec the item-regex until there are no more results @@ -140,17 +140,20 @@ class Sequence { if (re_results_item !== null) { const results = re_results_item.groups; - // remove all undefined values - Object.keys(results).forEach(key => results[key] === undefined && delete results[key]); - - // parse all remaining values - item_data = { ...item_data, ...parse_item_value_string(...Object.entries(results)[0]) }; + // only proceeds, when there are results + if (results !== undefined) { + // remove all undefined values + Object.keys(results).forEach(key => results[key] === undefined && delete results[key]); + + // parse all remaining values + item_data = { ...item_data, ...parse_item_value_string(...Object.entries(results)[0]) }; + } } } while (re_results_item !== null); // store the sequence-item - switch (item_data.Type) { + switch (item_data.type) { case "Song": this.sequence_items.push(new Song(item_data as SongProps)); break; @@ -195,7 +198,7 @@ class Sequence { return this.active_item_slide; } - set_active_slide(slide): number { + set_active_slide(slide: number): number { const response = this.active_sequence_item.set_active_slide(slide); this.casparcg_select_slide(this.active_slide); @@ -217,9 +220,9 @@ class Sequence { throw new RangeError(`steps must be -1 or 1, but is ${steps}`); } - let new_active_item_number = this.active_item_number + steps; + let new_active_item_number = this.active_item_number; // steps until there is a selectable item - while (!this.sequence_items[new_active_item_number].props.selectable) { + do { new_active_item_number += steps; // if the new_active_item_number is back at the start, break, since there are no selectable items @@ -230,7 +233,7 @@ class Sequence { // sanitize the item-number new_active_item_number = this.sanitize_item_number(new_active_item_number); - } + } while (!this.sequence_items[new_active_item_number].props.selectable); // new active item has negative index -> roll over to other end if (new_active_item_number < 0) { @@ -321,11 +324,12 @@ class Sequence { this.casparcg_connections.forEach(async (casparcg_connection, loop_index) => { // load the item into casparcg casparcg_connection.cgAdd({ + /* eslint-disable @typescript-eslint/naming-convention */ channel: Config.casparcg.connections[loop_index].channel, layer: Config.casparcg.connections[loop_index].layers[1], cgLayer: 0, playOnLoad: this.casparcg_visibility, - template: Config.casparcg.templates[this.active_sequence_item.props.Type], + template: Config.casparcg.templates[this.active_sequence_item.props.type], // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug data: JSON.stringify(JSON.stringify(await this.active_sequence_item.create_render_object(), (_key, val) => { if (typeof val === "string") { @@ -334,6 +338,7 @@ class Sequence { return val; } })) + /* eslint-enable @typescript-eslint/naming-convention */ }); }); } @@ -342,10 +347,12 @@ class Sequence { this.casparcg_connections.forEach((casparcg_connection, loop_index) => { // jump to the slide-number in casparcg casparcg_connection.cgInvoke({ + /* eslint-disable @typescript-eslint/naming-convention */ channel: Config.casparcg.connections[loop_index].channel, layer: Config.casparcg.connections[loop_index].layers[1], cgLayer: 0, method: `jump(${slide})` + /* eslint-enable @typescript-eslint/naming-convention */ }); }); } @@ -359,9 +366,11 @@ class Sequence { }); const options = { + /* eslint-disable @typescript-eslint/naming-convention */ channel: Config.casparcg.connections[loop_index].channel, layer: Config.casparcg.connections[loop_index].layers[1], cgLayer: 0 + /* eslint-enable @typescript-eslint/naming-convention */ }; this.casparcg_visibility = visibility; @@ -379,7 +388,7 @@ class Sequence { } get active_sequence_item(): SequenceItem { - return this.sequence_items[this.active_item_number]; + return this.sequence_items[this.active_item]; } get active_slide(): number { @@ -400,7 +409,7 @@ class Sequence { get state(): JGCPSend.State { return { command: "state", - activeItemSlide: this.active_item_slide, + active_item_slide: this.active_item_slide, visibility: this.visibility }; } @@ -409,9 +418,9 @@ class Sequence { // parse an individual sequence-item-value function parse_item_value_string(key: string, value: string): { [P in keyof ItemProps]?: ItemProps[P]; } { // remove line-breaks - value = value.replaceAll(/'\s\+\s+'/gm, ""); + value = value.replace(/'\s\+\s+'/gm, ""); // un-escape escaped characters - value = value.replaceAll(/'#(\d+)'/gm, (_match, group) => String.fromCharCode(group)); + value = value.replace(/'#(\d+)'/gm, (match, group) => String.fromCharCode(group)); const return_props: { [P in keyof ItemProps]?: ItemProps[P]; } = {}; @@ -419,7 +428,7 @@ function parse_item_value_string(key: string, value: string): { [P in keyof Item switch (key) { case "Data": // remove whitespace and linebreaks - return_props[key] = value.replaceAll(/\s+/gm, ""); + return_props[key] = value.replace(/\s+/gm, ""); break; case "VerseOrder": // split csv-line into an array @@ -428,7 +437,7 @@ function parse_item_value_string(key: string, value: string): { [P in keyof Item case "FileName": // assume the type from the file-extension if (path.extname(value) === ".sng") { - return_props.Type = "Song"; + return_props.type = "Song"; } return_props[key] = value; break; @@ -453,7 +462,7 @@ function parse_item_value_string(key: string, value: string): { [P in keyof Item break; case "StreamClass": if (value === "TPresentationObjectTimer") { - return_props.Type = "Countdown"; + return_props.type = "Countdown"; } else { return_props[key] = value; } diff --git a/src/server/SequenceItems/Comment.ts b/src/server/SequenceItems/Comment.ts index a960e0a..f627cd3 100644 --- a/src/server/SequenceItems/Comment.ts +++ b/src/server/SequenceItems/Comment.ts @@ -1,13 +1,20 @@ -import { ClientItemSlides, ItemProps, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; +import { ClientItemSlidesBase, ItemProps, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; export interface CommentProps extends ItemPropsBase { selectable: false; } +export interface ClientCommentSlides extends ClientItemSlidesBase { + type: "Comment"; +} + +export interface CommentRenderObject extends ItemRenderObjectBase { +} + export default class Comment extends SequenceItemBase { protected item_props: CommentProps; - protected SlideCount: number; + protected slide_count: number; constructor(props: CommentProps) { super(); @@ -17,11 +24,21 @@ export default class Comment extends SequenceItemBase { this.item_props.selectable = false; } - async create_client_object_item_slides(): Promise { - return undefined; + async create_client_object_item_slides(): Promise { + return { + type: "Comment", + title: this.props.Caption, + item: this.props.item, + slides: [], + slides_template: { + slides: [], + slide: 0, + mute_transition: true + } + }; } - async create_render_object(): Promise { + async create_render_object(): Promise { return undefined; } @@ -31,15 +48,15 @@ export default class Comment extends SequenceItemBase { } set_active_slide(): number { - return undefined; + return 0; } protected async get_background_image(): Promise { - return undefined; + return ""; } get active_slide(): number { - return undefined; + return -1; } get props(): ItemProps { diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts index 6266be0..a9558bc 100644 --- a/src/server/SequenceItems/Countdown.ts +++ b/src/server/SequenceItems/Countdown.ts @@ -1,32 +1,32 @@ import { convert_color_to_hex } from "../Sequence"; -import { ClientItemSlidesBase, FontFormat, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; +import { ClientItemSlidesBase, DeepPartial, FontFormat, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; -const countdown_mode_items = ["duration", "end-time", "stopwatch", "clock"]; +const countdown_mode_items = ["duration", "end_time", "stopwatch", "clock"]; type CountdownMode = (typeof countdown_mode_items)[number]; interface CountdownPosition { x: number, y: number } interface CountdownSequenceItemProps extends ItemPropsBase { - Type: "Countdown"; + /* eslint-disable @typescript-eslint/naming-convention */ + type: "Countdown"; Time: string; Data: string; + /* eslint-enable @typescript-eslint/naming-convention */ } export interface CountdownProps extends CountdownSequenceItemProps { - FontFormat: FontFormat; - Position: CountdownPosition; - Mode: CountdownMode; - showSeconds: boolean; - BackgroundImage?: { - path: string; - orig?: string; - proxy?: string; - }; - BackgroundColor?: string; + font_format: FontFormat; + position: CountdownPosition; + mode: CountdownMode; + show_seconds: boolean; + background: { + path?: string; + color?: string; + } } export interface ClientCountdownSlides extends ClientItemSlidesBase { - Type: "Countdown"; + type: "Countdown"; slides: [{ time: string; mode: CountdownMode; @@ -35,35 +35,37 @@ export interface ClientCountdownSlides extends ClientItemSlidesBase { } export interface CountdownRenderObject extends ItemRenderObjectBase { - slides: undefined; + slides: []; position: CountdownPosition; - fontFormat: FontFormat; + font_format: FontFormat; time: string; - showSeconds: boolean; + show_seconds: boolean; } // data from in the hex-string of the countdown, uses CSS-notation for easy translation in the renderer interface CountdownData { mode: CountdownMode; - fontFormat: { + font_format: { + /* eslint-disable @typescript-eslint/naming-convention */ fontFamily?: string; fontSize: number; color: string; fontWeight?: "bold"; fontStyle?: "italic"; textDecoration?: "underline"; + /* eslint-enable @typescript-eslint/naming-convention */ } x: number; y: number; - showSeconds: boolean; - backgroundImage?: string; - backgroundColor?: string; + show_seconds: boolean; + background_image?: string; + background_color?: string; } export default class Countdown extends SequenceItemBase { protected item_props: CountdownProps; - protected SlideCount: number = 1; + protected slide_count: number = 1; constructor(props: CountdownSequenceItemProps) { super(); @@ -72,17 +74,17 @@ export default class Countdown extends SequenceItemBase { this.item_props = { ...props, - Position: { + position: { x: hex_data.x, y: hex_data.y }, - showSeconds: hex_data.showSeconds, - FontFormat: hex_data.fontFormat, - Mode: hex_data.mode, - BackgroundImage: { - path: hex_data.backgroundImage - }, - BackgroundColor: hex_data.backgroundColor + show_seconds: hex_data.show_seconds, + font_format: hex_data.font_format, + mode: hex_data.mode, + background: { + path: hex_data.background_image, + color: hex_data.background_color + } }; } @@ -100,7 +102,7 @@ export default class Countdown extends SequenceItemBase { } set_active_slide(slide?: number): number { - this.validate_slide_number(slide); + slide = this.validate_slide_number(slide); return slide; } @@ -109,17 +111,17 @@ export default class Countdown extends SequenceItemBase { const title_map = { clock: "Clock", stopwatch: "Stopwatch", - duration: "Countdown (endtime)", - "end-time": "Countdown (duration)" + duration: "Countdown (duration)", + end_time: "Countdown (end time)" }; return { - title: `${title_map[this.item_props.Mode]}: ${this.item_props.Time}`, - Type: this.item_props.Type, - item: this.item_props.Item, + title: `${title_map[this.props.mode]}: ${this.props.Time}`, + type: this.props.type, + item: this.props.item, slides: [{ - mode: this.item_props.Mode, - time: this.item_props.Time + mode: this.props.mode, + time: this.props.Time }], slides_template: { ...await this.create_render_object(true), @@ -130,24 +132,23 @@ export default class Countdown extends SequenceItemBase { async create_render_object(proxy?: boolean): Promise { return { - backgroundImage: await this.get_background_image(proxy), - backgroundColor: this.item_props.BackgroundColor, + background_image: await this.get_background_image(proxy), slide: 0, - slides: undefined, - time: this.item_props.Time, - fontFormat: this.item_props.FontFormat, - position: this.item_props.Position, - showSeconds: this.item_props.showSeconds + slides: [], + time: this.props.Time, + font_format: this.props.font_format, + position: this.props.position, + show_seconds: this.props.show_seconds }; } protected async get_background_image(proxy?: boolean): Promise { // check wether the images have yet been laoded - if (this.props.backgroundImage === undefined) { - await this.load_backgroundImage(this.props.BackgroundImage.path); + if (this.props.BackgroundImage === undefined) { + await this.load_background_images(this.props.background.path, this.props.background.color); } - return this.props.backgroundImage[proxy ? "proxy" : "orig"]; + return this.props.BackgroundImage![proxy ? "proxy" : "orig"]; } get active_slide(): number { @@ -161,18 +162,19 @@ export default class Countdown extends SequenceItemBase { } function parse_hex_data(data_hex): CountdownData { - const regex_curse = /(?:546578745374796C6573060[1-3](?42)?(?49)?(?55)?|54657874436F6C6F72(?:(?:04|0707)(?[0-9A-F]{6}|(?:[0-9A-F]{2})+?)0)|466F6E744E616D650604(?(?:[A-F0-9]{2})+?)09|547970020(?[0-3])09|506F736974696F6E5802(?[A-F0-9]{2})09|506F736974696F6E5902(?[A-F0-9]{2})08|466F6E7453697A6502(?[A-F0-9]{2})0F|4261636B67726F756E64496D616765(?:[A-F0-9]{4}636F6C6F723A2F2F244646(?[A-F0-9]{12})|(?:[A-F0-9]{2})*?0[0-F]{3}(?(?:[0-9A-F]{2})+?))0|53686F775365636F6E647308(?.))/g; + const regex_curse = /(?:546578745374796C6573060[1-3](?42)?(?49)?(?55)?|54657874436F6C6F72(?:(?:04|0707)(?[0-9A-F]{6}|(?:[0-9A-F]{2})+?)0)|466F6E744E616D650604(?(?:[A-F0-9]{2})+?)09|547970020(?[0-3])09|506F736974696F6E5802(?[A-F0-9]{2})09|506F736974696F6E5902(?[A-F0-9]{2})08|466F6E7453697A6502(?[A-F0-9]{2})0F|4261636B67726F756E64496D616765(?:[A-F0-9]{4}636F6C6F723A2F2F244646(?[A-F0-9]{12})|(?:[A-F0-9]{2})*?0[0-F]{3}(?(?:[0-9A-F]{2})+?))0|53686F775365636F6E647308(?.))/g; - const data: CountdownData = { - fontFormat: { + const data: DeepPartial = { + font_format: { color: undefined, + // eslint-disable-next-line @typescript-eslint/naming-convention fontSize: undefined }, mode: undefined, x: undefined, y: undefined, - showSeconds: true + show_seconds: true }; const to_string = (raw) => Buffer.from(raw, "hex").toString(); @@ -198,35 +200,35 @@ function parse_hex_data(data_hex): CountdownData { data.mode = countdown_mode_items[Number(val)]; break; case "color": - data.fontFormat.color = to_rgb(val); + data.font_format!.color = to_rgb(val); break; - case "fontSize": - data.fontFormat.fontSize = to_int(val); + case "font_size": + data.font_format!.fontSize = to_int(val); break; case "x": case "y": data[key] = to_int(val); break; - case "showSeconds": - data.showSeconds = !val; + case "show_seconds": + data.show_seconds = !val; break; case "bold": - data.fontFormat.fontWeight = "bold"; + data.font_format!.fontWeight = "bold"; break; case "italic": - data.fontFormat.fontStyle = "italic"; + data.font_format!.fontStyle = "italic"; break; case "underline": - data.fontFormat.textDecoration = "underline"; + data.font_format!.textDecoration = "underline"; break; - case "fontFamily": - data.fontFormat.fontFamily = to_string(val); + case "font_family": + data.font_format!.fontFamily = to_string(val); break; - case "backgroundImage": - data.backgroundImage = to_string(val); + case "background_image": + data.background_image = to_string(val); break; - case "backgroundColor": - data.backgroundColor = `#${to_string(val)}`; + case "background_color": + data.background_color = `#${to_string(val)}`; break; default: console.error(`countdown_data['${key}'] is not implemented yet`); @@ -236,5 +238,5 @@ function parse_hex_data(data_hex): CountdownData { }); } - return data; + return data as CountdownData; } \ No newline at end of file diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index fb129f7..c2a9dcc 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -6,58 +6,66 @@ import sharp from "sharp"; import Config from "../config"; import Song, { ClientSongSlides, SongProps, SongRenderObject } from "./Song"; import Countdown, { ClientCountdownSlides, CountdownProps, CountdownRenderObject } from "./Countdown"; -import Comment from "./Comment"; +import Comment, { ClientCommentSlides, CommentProps, CommentRenderObject } from "./Comment"; export type SequenceItem = Song | Countdown | Comment; +export type DeepPartial = { + [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; +}; + export interface ItemPropsBase { - Type: string; + /* eslint-disable @typescript-eslint/naming-convention */ + type: string; Caption: string; - SlideCount: number; + slide_count: number; Color: string; - Item: number; + item: number; selectable: boolean; - backgroundImage?: { + BackgroundImage?: { orig: string; proxy: string; } + /* eslint-enable @typescript-eslint/naming-convention */ } -export type ItemProps = ItemPropsBase | SongProps | CountdownProps +export type ItemProps = SongProps | CountdownProps | CommentProps; // interface for a renderer-object export interface ItemRenderObjectBase { slides: Array; slide: number; - backgroundImage?: string; - backgroundColor?: string; + background_image?: string; + background_color?: string; mute_transition?: boolean; } -export type ItemRenderObject = ItemRenderObjectBase | SongRenderObject | CountdownRenderObject; +export type ItemRenderObject = SongRenderObject | CountdownRenderObject | CommentRenderObject; export interface ClientItemSlidesBase { - Type: string; + type: string; title: string; item: number; slides: object; slides_template: ItemRenderObject & { mute_transition: true; }; } -export type ClientItemSlides = ClientItemSlidesBase | ClientSongSlides | ClientCountdownSlides; +export type ClientItemSlides = ClientSongSlides | ClientCountdownSlides | ClientCommentSlides; export interface FontFormat { + /* eslint-disable @typescript-eslint/naming-convention */ fontFamily?: string; fontSize: number; fontWeight?: "bold"; fontStyle?: "italic"; fontDecoration?: "underline"; color: string; + /* eslint-enable @typescript-eslint/naming-convention */ } export abstract class SequenceItemBase { protected abstract item_props: ItemProps; - protected abstract SlideCount: number; + protected abstract slide_count: number; abstract create_render_object(proxy?: boolean, slide?: number); abstract create_client_object_item_slides(): Promise; @@ -72,8 +80,8 @@ export abstract class SequenceItemBase { abstract get active_slide(): number; - protected validate_slide_number(slide: number): number { - const slide_count = this.SlideCount; + protected validate_slide_number(slide: unknown): number { + const slide_count = this.slide_count; if (typeof slide !== "number") { throw new TypeError(`'${slide} is not of type 'number'`); @@ -90,23 +98,39 @@ export abstract class SequenceItemBase { return slide; } - protected async load_backgroundImage(image_path: string) { - let img_buffer; + protected async load_background_images(image_path?: string, background_color?: string) { + let img_buffer, img_buffer_proxy; - try { - img_buffer = await fs.readFile(path.join(Config.path.backgroundImage, image_path)); - } catch (e) { - this.item_props.backgroundImage = { - orig: "", - proxy: "" - }; - - return; + if (image_path !== undefined) { + try { + img_buffer = await fs.readFile(path.join(Config.path.background_image, image_path)); + } catch (e) { + this.item_props.BackgroundImage = { + orig: "", + proxy: "" + }; + + return; + } + img_buffer_proxy = sharp(img_buffer).resize(240).toBuffer(); + } + + // if the image_buffer is still undefined, try to use the backgroundColor + if (background_color !== undefined) { + img_buffer = sharp({ + create: { + width: 1, + height: 1, + channels: 4, + background: color_string_to_object(background_color) + } + }).png().toBuffer(); + + // copy the the image to the proxy buffer, since only 1px anyway + img_buffer_proxy = img_buffer; } - - const img_buffer_proxy = sharp(img_buffer).resize(240).toBuffer(); - this.item_props.backgroundImage = { + this.item_props.BackgroundImage = { orig: `data:${mime.lookup(image_path)};base64,` + (img_buffer).toString("base64"), proxy: `data:${mime.lookup(image_path)};base64,` + (await img_buffer_proxy).toString("base64") }; @@ -117,4 +141,19 @@ export abstract class SequenceItemBase { } protected abstract get_background_image(proxy?: boolean): Promise; +} + +function color_string_to_object(color: string): { r: number, g: number, b: number, alpha?: number} { + const regex_result = color.match(/#(?[0-9A-Fa-f]{2})(?[0-9A-Fa-f]{2})(?[0-9A-Fa-f]{2})(?[0-9A-Fa-f]{2})?/mg); + + if (!regex_result) { + throw new SyntaxError(`'${color}' is no valid color`); + } + + return { + r: Number(regex_result.groups?.r), + g: Number(regex_result.groups?.g), + b: Number(regex_result.groups?.b), + alpha: Number(regex_result.groups?.alpha) + }; } \ No newline at end of file diff --git a/src/server/SequenceItems/Song.ts b/src/server/SequenceItems/Song.ts index 76efe18..e498a31 100644 --- a/src/server/SequenceItems/Song.ts +++ b/src/server/SequenceItems/Song.ts @@ -1,26 +1,28 @@ import { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; -import SongFile, { ItemPartClient, LyricPart, TitlePart } from "./SongFile"; +import SongFile, { ItemPartClient, LyricPart, LyricPartClient, TitlePart } from "./SongFile"; import path from "path"; import Config from "../config"; export interface SongProps extends ItemPropsBase { - Type: "Song"; + /* eslint-disable @typescript-eslint/naming-convention */ + type: "Song"; FileName: string; VerseOrder?: string[]; Language?: number; PrimaryLanguage?: number; + /* eslint-enable @typescript-eslint/naming-convention */ } -interface TitleSlide extends TitlePart { +export interface TitleSlide extends TitlePart { } -interface LyricSlide { +export interface LyricSlide { type: "lyric"; data: string[][]; } -type ItemSlide = LyricSlide | TitleSlide; +export type ItemSlide = LyricSlide | TitleSlide; export interface SongRenderObject extends ItemRenderObjectBase { type: "Song"; @@ -29,7 +31,7 @@ export interface SongRenderObject extends ItemRenderObjectBase { } export interface ClientSongSlides extends ClientItemSlidesBase { - Type: "Song" + type: "Song" slides: ItemPartClient[]; slides_template: SongRenderObject & { mute_transition: true; }; } @@ -38,13 +40,13 @@ export default class Song extends SequenceItemBase { protected item_props: SongProps; // amount of slides this element has - protected SlideCount: number = 0; + protected slide_count: number = 0; // currently active slide-number private active_slide_number: number = 0; private languages: number[]; - private SongFile: SongFile; + private song_file: SongFile; constructor(props: SongProps) { super(); @@ -52,12 +54,12 @@ export default class Song extends SequenceItemBase { this.item_props = props; try { - this.SongFile = new SongFile(get_song_path(props.FileName)); + this.song_file = new SongFile(get_song_path(props.FileName)); } catch (e) { // if the error is because the file doesn't exist, skip the rest of the loop iteration if (e.code === "ENOENT") { console.debug(`song '${props.FileName}' does not exist`); - return null; + return; } else { throw e; } @@ -65,7 +67,7 @@ export default class Song extends SequenceItemBase { // create the languages-array // initialize the array with all languages - this.languages = Array.from(Array(this.SongFile.languages).keys()); + this.languages = Array.from(Array(this.song_file.languages).keys()); // if there is a 'PrimaryLanguage' specified, move it to the first position if (this.item_props.PrimaryLanguage !== undefined) { @@ -78,13 +80,13 @@ export default class Song extends SequenceItemBase { } // add the title-slide to the counter - this.SlideCount++; + this.slide_count++; // count the slides for (const part of this.get_verse_order()) { // check wether the part is actually defined in the songfile try { - this.SlideCount += this.SongFile.get_part(part).slides.length; + this.slide_count += this.song_file.get_part(part).slides.length; } catch (e) { if (!(e instanceof ReferenceError)) { throw e; @@ -96,8 +98,10 @@ export default class Song extends SequenceItemBase { get_verse_order(): string[] { if (this.item_props.VerseOrder !== undefined) { return this.item_props.VerseOrder; + } else if (this.song_file.metadata.VerseOrder !== undefined) { + return this.song_file.metadata.VerseOrder; } else { - return this.SongFile.metadata.VerseOrder; + return []; } } @@ -111,18 +115,18 @@ export default class Song extends SequenceItemBase { const return_object: SongRenderObject = { type: "Song", slides: [ - this.SongFile.part_title + this.song_file.part_title ], slide, languages: this.languages, - backgroundImage: await this.get_background_image(proxy) + background_image: await this.get_background_image(proxy) }; // add the individual parts to the output-object for (const part_name of this.get_verse_order()) { - let part: LyricPart = undefined; + let part: LyricPart | undefined = undefined; try { - part = this.SongFile.get_part(part_name); + part = this.song_file.get_part(part_name); } catch (e) { if (!(e instanceof ReferenceError)) { throw e; @@ -173,7 +177,7 @@ export default class Song extends SequenceItemBase { slide_steps = -1; // index is bigger than the slide-count -> roll over to zero - } else if (new_active_slide_number >= this.SlideCount) { + } else if (new_active_slide_number >= this.slide_count) { slide_steps = 1; } else { this.active_slide_number = new_active_slide_number; @@ -184,11 +188,11 @@ export default class Song extends SequenceItemBase { async create_client_object_item_slides(): Promise { const return_item: ClientSongSlides = { - Type: "Song", + type: "Song", title: this.item_props.Caption, - item: this.item_props.Item, + item: this.item_props.item, slides: [ - this.SongFile.get_title_client() + this.song_file.get_title_client() ], slides_template: { ...await this.create_render_object(true, 0), @@ -197,10 +201,10 @@ export default class Song extends SequenceItemBase { }; for (const part_name of this.get_verse_order()) { - let part = undefined; + let part: LyricPartClient | undefined = undefined; try { - part = this.SongFile.get_part_client(part_name); + part = this.song_file.get_part_client(part_name); } catch (e) { if (!(e instanceof ReferenceError)) { throw e; @@ -218,11 +222,11 @@ export default class Song extends SequenceItemBase { protected async get_background_image(proxy?: boolean): Promise { // check wether the images have yet been laoded - if (this.props.backgroundImage === undefined) { - await this.load_backgroundImage(this.SongFile.metadata.BackgroundImage); + if (this.props.BackgroundImage === undefined) { + await this.load_background_images(this.song_file.metadata.BackgroundImage); } - return this.props.backgroundImage[proxy ? "proxy" : "orig"]; + return this.props.BackgroundImage![proxy ? "proxy" : "orig"]; } get active_slide(): number { diff --git a/src/server/SequenceItems/SongFile.ts b/src/server/SequenceItems/SongFile.ts index b710ba2..65daa12 100644 --- a/src/server/SequenceItems/SongFile.ts +++ b/src/server/SequenceItems/SongFile.ts @@ -30,7 +30,7 @@ const verse_types = [ type SongElement = (typeof verse_types)[number]; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const isSongElement = (x: any): x is SongElement => { +const is_song_element = (x: any): x is SongElement => { if (typeof x !== "string") { return false; } @@ -42,6 +42,7 @@ const isSongElement = (x: any): x is SongElement => { // metadata of the songfile interface SongFileMetadata { + /* eslint-disable @typescript-eslint/naming-convention */ Title: string[]; ChurchSongID?: string; Songbook?: string; @@ -51,13 +52,14 @@ interface SongFileMetadata { Melody?: string; Translation?: string; Copyright?: string; - LangCount?: number; + LangCount: number; + /* eslint-enable @typescript-eslint/naming-convention */ } interface TitlePart { type: "title"; title: string[]; - ChurchSongID?: string; + church_song_id?: string; } interface LyricPart { @@ -92,7 +94,10 @@ class SongFile { private text: Record = {}; metadata: SongFileMetadata = { - Title: [] + /* eslint-disable @typescript-eslint/naming-convention */ + Title: [], + LangCount: 1 // set it by default to 1. if there are more languages, they will be read from the header + /* eslint-enable @typescript-eslint/naming-convention */ }; constructor(path: string) { @@ -115,7 +120,7 @@ class SongFile { header_data.forEach((row) => { const components = row.split("="); - const [key, value] = [components.shift().substring(1), components.join("=")]; + const [key, value] = [components.shift()?.substring(1), components.join("=")]; // handle different data differently switch (key) { @@ -189,7 +194,7 @@ class SongFile { // check if the first row describes the part if ( - isSongElement(first_line_items[0].toLowerCase()) && + is_song_element(first_line_items[0].toLowerCase()) && first_line_items.length <= 2 && first_line_items.length > 0 ) { @@ -225,7 +230,7 @@ class SongFile { }; if (this.metadata.ChurchSongID !== undefined) { - response.ChurchSongID = this.metadata.ChurchSongID; + response.church_song_id = this.metadata.ChurchSongID; } return response; @@ -233,7 +238,7 @@ class SongFile { // returns the different parts of the song get_part(part: string): LyricPart { - if (!isSongElement(part) || !this.avaliable_parts.includes(part)) { + if (!is_song_element(part) || !this.avaliable_parts.includes(part)) { throw new ReferenceError(`'${part}' is no valid song-part`); } @@ -254,7 +259,7 @@ class SongFile { } get_part_client(part: string): LyricPartClient { - if (!isSongElement(part) || !this.avaliable_parts.includes(part)) { + if (!is_song_element(part) || !this.avaliable_parts.includes(part)) { throw new ReferenceError(`'${part}' is no valid song-part`); } diff --git a/src/server/config.ts b/src/server/config.ts index 10511db..f1cd179 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -1,15 +1,17 @@ interface ConfigJSON { behaviour: { - showOnLoad: boolean; + show_on_load: boolean; }; path: { - backgroundImage: string; + background_image: string; song: string; }; casparcg: { templates: { + /* eslint-disable @typescript-eslint/naming-convention */ Song: string, Countdown: string + /* eslint-enable @typescript-eslint/naming-convention */ }; connections: { host: string; @@ -18,7 +20,7 @@ interface ConfigJSON { layers: [number, number]; }[]; }; - clientServer: { + client_server: { http: { port: number; }; @@ -26,12 +28,12 @@ interface ConfigJSON { port: number; }; }; - oscServer: { + osc_server: { port: number; }; companion: { address: string; - oscPort: number; + osc_port: number; } } @@ -41,6 +43,8 @@ import fs from "fs"; const config_path = "config.json"; + +// eslint-disable-next-line @typescript-eslint/naming-convention const Config: ConfigJSON = JSON.parse(fs.readFileSync(config_path, { encoding: "utf-8" })); export default Config; diff --git a/src/server/control.ts b/src/server/control.ts index 552171e..f1fb174 100644 --- a/src/server/control.ts +++ b/src/server/control.ts @@ -15,12 +15,12 @@ class Control { // mapping of the OSC-commands to the functions private readonly osc_function_map: OSCFunctionMap = { control: { - "sequence-item": { + sequence_item: { navigate: { direction: (value: number) => this.navigate("item", value) } }, - "item-slide": { + item_slide: { navigate: { direction: (value: number) => this.navigate("slide", value) } @@ -33,14 +33,15 @@ class Control { // mapping of the websocket-messages to the functions private readonly ws_function_map = { - "open-sequence": (msg: JGCPRecv.OpenSequence, ws: WebSocket) => this.open_sequence(msg?.sequence, ws), - "request-item-slides": (msg: JGCPRecv.RequestItemSlides, ws: WebSocket) => this.get_item_slides(msg?.item, msg?.clientID, ws), - "select-item-slide": (msg: JGCPRecv.ItemSlideSelect, ws: WebSocket) => this.select_item_slide(msg?.item, msg?.slide, msg?.clientID, ws), - navigate: (msg: JGCPRecv.Navigate, ws: WebSocket) => this.navigate(msg?.type, msg?.steps, msg?.clientID, ws), - "set-visibility": (msg: JGCPRecv.SetVisibility, ws: WebSocket) => this.set_visibility(msg.visibility, msg.clientID, ws) + open_sequence: (msg: JGCPRecv.OpenSequence, ws: WebSocket) => this.open_sequence(msg?.sequence, ws), + request_item_slides: (msg: JGCPRecv.RequestItemSlides, ws: WebSocket) => this.get_item_slides(msg?.item, msg?.client_id, ws), + select_item_slide: (msg: JGCPRecv.ItemSlideSelect, ws: WebSocket) => this.select_item_slide(msg?.item, msg?.slide, msg?.client_id, ws), + navigate: (msg: JGCPRecv.Navigate, ws: WebSocket) => this.navigate(msg?.type, msg?.steps, msg?.client_id, ws), + set_visibility: (msg: JGCPRecv.SetVisibility, ws: WebSocket) => this.set_visibility(msg.visibility, msg.client_id, ws) }; private readonly ws_message_handler: WebsocketMessageHandler = { + // eslint-disable-next-line @typescript-eslint/naming-convention JGCP: { connection: (ws: WebSocket) => this.ws_on_connection(ws), message: (ws: WebSocket, data: RawData) => this.ws_on_message(ws, data) @@ -66,12 +67,12 @@ class Control { this.sequence = new Sequence(sequence); // send the sequence to all clients - const response_sequenceItems: JGCPSend.Sequence = { - command: "sequence-items", + const response_sequence_items: JGCPSend.Sequence = { + command: "sequence_items", ...this.sequence.create_client_object_sequence() }; - this.send_all_clients(response_sequenceItems); + this.send_all_clients(response_sequence_items); // send the current state to all clients this.send_all_clients(this.sequence.state); @@ -82,15 +83,15 @@ class Control { /** * Reply on a item-slides-request with the requested item-slides * @param item - * @param clientID + * @param client_id * @param ws */ - private async get_item_slides(item: number, clientID?: string, ws?: WebSocket) { + private async get_item_slides(item: number, client_id?: string, ws?: WebSocket) { // type-check the item if (typeof item === "number") { const message: JGCPSend.ItemSlides = { - command: "item-slides", - clientID, + command: "item_slides", + client_id: client_id!, ...await this.sequence.create_client_object_item_slides(item) }; @@ -102,7 +103,7 @@ class Control { } } - private select_item_slide(item: number, slide: number, clientID?: string, ws?: WebSocket) { + private select_item_slide(item: number, slide: number, client_id?: string, ws?: WebSocket) { if (typeof item !== "number") { ws_send_response("'item' is not of type number", false, ws); } @@ -131,8 +132,8 @@ class Control { this.send_all_clients({ command: "state", - activeItemSlide: this.sequence.active_item_slide, - clientID + active_item_slide: this.sequence.active_item_slide, + client_id: client_id }); ws_send_response("slide has been selected", true, ws); @@ -142,11 +143,17 @@ class Control { * navigate the active item or slide forwards or backwards * @param type * @param steps - * @param clientID + * @param client_id */ - private navigate(type: JGCPRecv.NavigateType, steps: number, clientID?: string, ws?: WebSocket) { - if (!JGCPRecv.isItemNavigateType(type)) { - ws_send_response(`'type' has to be one of ${JSON.stringify(JGCPRecv.isItemNavigateType)}`, false, ws); + private navigate(type: JGCPRecv.NavigateType, steps: number, client_id?: string, ws?: WebSocket) { + // if there is no sequence loaded, send a negative response back and exit + if (this.sequence === undefined) { + ws_send_response("no schedule loaded", false, ws); + return; + } + + if (!JGCPRecv.is_item_navigate_type(type)) { + ws_send_response(`'type' has to be one of ${JSON.stringify(JGCPRecv.is_item_navigate_type)}`, false, ws); } if (![-1, 1].includes(steps)) { @@ -163,8 +170,8 @@ class Control { this.send_all_clients({ command: "state", - activeItemSlide: this.sequence.active_item_slide, - clientID + active_item_slide: this.sequence.active_item_slide, + client_id: client_id }); ws_send_response(`'${type}' has been navigated`, true, ws); @@ -174,7 +181,7 @@ class Control { * set the visibility of the sequence in the renderer * @param visibility wether the output should be visible (true) or not (false) */ - private set_visibility(visibility: boolean, _clientID?: string, ws?: WebSocket) { + private set_visibility(visibility: boolean, client_id?: string, ws?: WebSocket) { // if there is no sequence loaded, send a negative response back and exit if (this.sequence === undefined) { ws_send_response("no schedule loaded", false, ws); @@ -222,7 +229,7 @@ class Control { if (this.sequence !== undefined) { // send the sequence const respone_sequence: JGCPSend.Sequence = { - command: "sequence-items", + command: "sequence_items", ...this.sequence.create_client_object_sequence() }; ws.send(JSON.stringify(respone_sequence)); @@ -277,7 +284,7 @@ class Control { * @param success status code 200 = SUCCESS (true) or 400 = ERROR (false) * @param ws */ -function ws_send_response(message: string, success: boolean, ws: WebSocket) { +function ws_send_response(message: string, success: boolean, ws?: WebSocket) { const response = { command: "response", message: message, diff --git a/src/server/main.ts b/src/server/main.ts index 66d1651..4a21d7b 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -1,15 +1,16 @@ -import { http_server } from "./servers/http-server"; +import HTTPServer from "./servers/http-server"; + import Control from "./control"; import Config from "./config"; // http-server -const server = new http_server(Config.clientServer.http.port); +const server = new HTTPServer(Config.client_server.http.port); -new Control(Config.clientServer.websocket, { - portReceive: Config.oscServer.port, - addressSend: Config.companion.address, - portSend: Config.companion.oscPort +new Control(Config.client_server.websocket, { + port_receive: Config.osc_server.port, + address_send: Config.companion.address, + port_send: Config.companion.osc_port }); server.start(); diff --git a/src/server/servers/http-server.ts b/src/server/servers/http-server.ts index 9587f61..176eda9 100644 --- a/src/server/servers/http-server.ts +++ b/src/server/servers/http-server.ts @@ -6,7 +6,7 @@ import { unescape } from "querystring"; import Config from "../config"; -class http_server { +class HTTPServer { private port: number; server: http.Server; @@ -20,7 +20,7 @@ class http_server { let resource_dir = "client"; // unescape the percent signs in the url - request.url = unescape(request.url); + request.url = unescape(request.url!); // override different requested urls switch (true) { @@ -35,7 +35,7 @@ class http_server { break; // serve the background-images case /^\/BackgroundImage\//.test(request.url): - resource_dir = Config.path.backgroundImage; + resource_dir = Config.path.background_image; request.url = request.url.replace(/\/BackgroundImage\//, ""); break; } @@ -45,12 +45,14 @@ class http_server { // if there was an error while opening the file, serve a 404-error if (err) { response.writeHead(404, { + // eslint-disable-next-line @typescript-eslint/naming-convention "Content-Type": "text/plain" }); response.write("Resource not found"); } else { response.writeHead(200, { + // eslint-disable-next-line @typescript-eslint/naming-convention "Content-Type": mime.lookup(request.url) }); @@ -64,4 +66,4 @@ class http_server { } } -export { http_server }; +export default HTTPServer; diff --git a/src/server/servers/osc-server.ts b/src/server/servers/osc-server.ts index 5534727..f1ffaf1 100644 --- a/src/server/servers/osc-server.ts +++ b/src/server/servers/osc-server.ts @@ -1,14 +1,14 @@ import osc from "osc"; interface OSCServerArguments { - portReceive: number; - addressSend: string; - portSend: number; + port_receive: number; + address_send: string; + port_send: number; } type OSCValueType = boolean | number | string; -type OSCFunctionMap = { [key: string]: OSCFunctionMap | ((value: OSCValueType) => void ) }; +type OSCFunctionMap = { [key: string]: OSCFunctionMap | ((value: boolean) => void ) | ((value: number) => void ) | ((value: string) => void ) }; class OSCServer { private osc_server: osc.UDPPort; @@ -19,20 +19,22 @@ class OSCServer { this.function_map = function_map; this.osc_server = new osc.UDPPort({ + /* eslint-disable @typescript-eslint/naming-convention */ localAddress: "0.0.0.0", - localPort: args.portReceive, - remoteAddresse: args.addressSend, - remotePort: args.portSend + localPort: args.port_receive, + remoteAddresse: args.address_send, + remotePort: args.port_send + /* eslint-enable @typescript-eslint/naming-convention */ }); - this.osc_server.on("message", (oscMsg) => { - const parts = oscMsg.address.split("/"); + this.osc_server.on("message", (osc_msg) => { + const parts = osc_msg.address.split("/"); // remove the first empty elementn from the leading slash parts.shift(); // execute the command map - this.execute_command(parts, this.function_map, oscMsg.args[0]); + this.execute_command(parts, this.function_map, osc_msg.args[0]); }); this.osc_server.open(); @@ -40,7 +42,7 @@ class OSCServer { private execute_command(path: string[], command_tree, value) { if (path.length > 1) { - const traversed_command_tree = command_tree[path.shift()]; + const traversed_command_tree = command_tree[path.shift()!]; if (traversed_command_tree !== undefined) { this.execute_command(path, traversed_command_tree, value); diff --git a/src/server/servers/websocket-server.ts b/src/server/servers/websocket-server.ts index 872fde4..533ef2f 100644 --- a/src/server/servers/websocket-server.ts +++ b/src/server/servers/websocket-server.ts @@ -15,6 +15,7 @@ interface MessageHandler { pong?: (ws: WebSocket, data: Buffer) => void; error?: (ws: WebSocket, err: Error) => void; close?: (ws: WebSocket, reason: Buffer) => void; + // eslint-disable-next-line @typescript-eslint/naming-convention "unexpected-response"?: (ws: WebSocket, request: ClientRequest, response: IncomingMessage) => void; upgrade?: (ws: WebSocket, request: IncomingMessage) => void; connection?: (ws: WebSocket, socket: WebSocket, request: IncomingMessage) => void; @@ -58,7 +59,7 @@ class WebsocketServer { // execute the on_connection function if (this.message_handlers[ws.protocol].connection !== undefined) { - this.message_handlers[ws.protocol].connection(ws, socket, request); + this.message_handlers[ws.protocol].connection!(ws, socket, request); } } else { // reject connection diff --git a/src/templates/Countdown.ts b/src/templates/Countdown.ts new file mode 100644 index 0000000..8ebf66d --- /dev/null +++ b/src/templates/Countdown.ts @@ -0,0 +1,212 @@ +import { CountdownRenderObject } from "../server/SequenceItems/Countdown"; + +let update_interval; +const spans: { + hours?: HTMLSpanElement[], + minutes: HTMLSpanElement[], + seconds?: HTMLSpanElement[] +} = { + hours: [], + minutes: [] +}; +let data: CountdownRenderObject; + +const end_time = new Date(); + +let end_time_sign; + + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function update(str_args) { + // clear the old-state + clearInterval(update_interval); + + data = JSON.parse(str_args); + + // if requested, diable transition-effects + const main_div = document.querySelector("div#main"); + if (main_div !== null) { + if (data.mute_transition) { + main_div.style.transitionProperty = "none"; + } else { + main_div.style.transitionProperty = ""; + } + } + + // create the individual spans for the numbers + spans.hours = [ + document.createElement("span"), + document.createElement("span") + ]; + spans.minutes = [ + document.createElement("span"), + document.createElement("span") + ]; + + if (data.show_seconds === true) { + spans.seconds = [ + document.createElement("span"), + document.createElement("span") + ]; + } + + // create the individual spans + const time_div = document.querySelector("#time"); + if (time_div === null) { + return; + } + + const colon_hm: HTMLSpanElement = document.createElement("span"); + colon_hm.innerText = ":"; + colon_hm.id = "colon_hm"; + colon_hm.classList.add("colon"); + + Object.entries(spans).forEach(([key, vals]) => { + if (key === "seconds") { + const colon_ms = colon_hm.cloneNode(true) as HTMLSpanElement; + colon_ms.id = "colon_ms"; + time_div.append(colon_ms); + + } + + time_div.append(vals[0]); + time_div.append(vals[1]); + + if (key === "hours") { + time_div.append(colon_hm); + } + }); + + // if the position is undefined, set them to center + if (data.position.x === undefined) { + data.position.x = 50; + } + if (data.position.y === undefined) { + data.position.y = 50; + } + + if (data.font_format !== undefined) { + const this_format = { + ...data.font_format, + // eslint-disable-next-line @typescript-eslint/naming-convention + fontSize: `${data.font_format.fontSize}em` + }; + + Object.entries(this_format).forEach(([key, val]) => { + time_div.style[key] = val; + }); + } + + if (main_div !== null) { + if (data.background_image !== undefined) { + main_div.style.backgroundImage = `url("${data.background_image.replace(/\\/g, "\\\\")}")`; + } + if (data.background_color !== undefined) { + main_div.style.backgroundImage = data.background_color; + } + } + + if (data.time !== undefined) { + const time = data.time.match(/(?\d+)(?::)(?\d\d)((?::)(?\d\d))?/); + + if (time?.groups) { + end_time.setHours(parseInt(time.groups.hours)); + end_time.setMinutes(parseInt(time.groups.minutes)); + end_time.setSeconds(parseInt(time.groups.seconds)); + } + + end_time_sign = Math.sign(Date.now() - end_time.getTime()); + + update_interval = setInterval(function () { + update_time(); + }, 100); + position_time(); + update_time(); + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function play() { + document.querySelector("#main")?.classList.remove("stop"); + document.querySelector("#main")?.classList.add("show"); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function stop() { + document.querySelector("#main")?.classList.remove("show"); + document.querySelector("#main")?.classList.add("stop"); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function next() { +} + +function get_remaining_time(): [{ hours: string[], minutes: string[], seconds: string[] }, boolean] { + const time_remaining = new Date(end_time.getTime() - Date.now()); + + return [ + { + hours: time_remaining.getUTCHours().toString().padStart(2, "0").split(""), + minutes: time_remaining.getUTCMinutes().toString().padStart(2, "0").split(""), + seconds: time_remaining.getUTCSeconds().toString().padStart(2, "0").split("") + }, + Math.sign(time_remaining.getTime()) === end_time_sign + ]; +} + +const root = document.querySelector(":root"); +const time_div = document.querySelector("div#time"); + +function position_time() { + const root_width = root?.clientWidth; + const root_height = root?.clientHeight; + const time_width = time_div?.clientWidth; + const time_height = time_div?.clientHeight; + + if ( + root_width !== undefined && + root_height !== undefined && + time_width !== undefined && + time_height !== undefined && + time_div !== null + ) { + + const free_width_share = 1 - time_width / root_width; + const free_height_share = 1 - time_height / root_height; + + const left = data.position.x * free_width_share + time_width / root_width / 2; + const top = data.position.y * free_height_share + time_height / root_height / 2; + + time_div.style.left = `${left}%`; + time_div.style.top = `${top}%`; + + time_div.style.transform = `translate(-${left}, -${top})`; + } +} + +function update_time() { + const [time, finished] = get_remaining_time(); + + if (finished) { + clearInterval(update_interval); + } else { + // if the hours are 00, don't show them anymore + if (time.hours.join("") === "00") { + if (spans.hours !== undefined) { + spans.hours[0].remove(); + spans.hours[1].remove(); + document.querySelector("#colon_hm")?.remove(); + + delete spans.hours; + + // re-position everything + position_time(); + } + } + + Object.entries(spans).forEach(([key, val]) => { + val[0].innerText = time[key][0]; + val[1].innerText = time[key][1]; + }); + } +} \ No newline at end of file diff --git a/src/templates/Song.ts b/src/templates/Song.ts new file mode 100644 index 0000000..111fa7a --- /dev/null +++ b/src/templates/Song.ts @@ -0,0 +1,210 @@ +import { ItemSlide, SongRenderObject } from "../server/SequenceItems/Song"; + +let data: SongRenderObject; + +let active_slide = 0; +let slide_count = 0; + +// casparcg-function: transmits data +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function update(s_data: string) { + // parse the transferred data into json + data = JSON.parse(s_data); + + // get the div for the display and storage + const div_container = document.querySelector("div#container"); + const div_storage = document.querySelector("div#storage"); + + if (div_container === null || div_storage === null) return; + + div_container.style.backgroundImage = `url("${data.background_image}")`; + + // if requested, diable transition-effects + const main_div = document.querySelector("div#_main"); + if (main_div === null) return; + + if (data.mute_transition) { + main_div.style.transitionProperty = "none"; + } else { + main_div.style.transitionProperty = ""; + } + + // clear the storage-container + div_storage.innerHTML = ""; + + // counter for the individual slides + let slide_counter = 0; + + // create the slides and store them in the container + for (const slide_data of data["slides"]) { + // parent-div of slide + const div_slide = create_slide(slide_data, data.languages); + div_slide.dataset.slide = slide_counter.toString(); + div_storage.append(div_slide); + + slide_counter++; + } + + // store the amount of slides + slide_count = slide_counter; + + active_slide = data.slide; + + // display the first slide + jump(active_slide); + + // resize all the slides (has to be done after displayin the first slide) + resize_slides(); +} + +// casparcg-function: displays the template +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function play() { + const main_div = document.querySelector("div#_main"); + + if (main_div !== null) { + main_div.style.opacity = "1"; + } +} + +// casparcg-function: advances to the next step +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function next() { + jump(active_slide + 1); +} + +// custom-function (through invoke): advance to the previous step +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function prev() { + jump(active_slide - 1); +} + +// custom-function (through invoke): jump to an arbitrary slide +function jump(counter_raw: number) { + const counter = clamp_slide_counter(counter_raw); + + // get the container to display the load the slide into + const main_div = document.querySelector("div#container"); + + if (main_div === null) return; + + // clear the content of the container + main_div.innerHTML = ""; + + // clone the slide and change its id, so the id stays unique + const div_slide = document.querySelector(`div[data-slide='${counter}']`)?.cloneNode(true) as HTMLDivElement; + + if (div_slide === undefined) return; + + div_slide.id = "slide_active"; + + main_div.appendChild(div_slide); + + active_slide = counter; +} + +// casparcg-function: hide the template +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function stop() { + const main_div = document.querySelector("div#_main"); + + if (main_div === null) return; + + main_div.style.opacity = "0"; +} + +// clamp the counter to valid values +function clamp_slide_counter(counter_raw: number) { + return Math.max(0, Math.min(slide_count - 1, counter_raw)); +} + +// change the font-size so the slide with the highest width uses all of the available space +function resize_slides() { + // get the highest width + const max_width = get_max_width(); + + // get the width of the container + const active_slide = document.querySelector("#slide_active"); + + resize_slides_text(max_width, active_slide?.clientWidth ?? 0); +} + +// get the width of the widest slide +function get_max_width() { + let max_width = 0; + + // get the width of the individual slides and store the biggest one + for (let ii = 0; ii < slide_count; ii++) { + const slide = document.querySelector(`div#storage [data-slide='${ii}']`); + + if (slide?.querySelector("div.lyric") !== undefined) { + const current_width = slide.clientWidth; + max_width = Math.max(current_width, max_width); + } + } + + return max_width; +} + +function create_slide(slide_data: ItemSlide, languages: number[]) { + // parent-div of slide + const div_slide = document.createElement("div"); + + const div_content = document.createElement("div"); + div_content.classList.add("slide"); + div_slide.append(div_content); + + switch (slide_data.type) { + case "title": { + div_content.classList.add("title"); + const title_container = document.createElement("div"); + title_container.classList.add("title_container"); + div_content.append(title_container); + + // create the titles for the individual languages + languages.forEach((language, index) => { + const div_title = document.createElement("div"); + div_title.classList.add(`language_${index}`); + div_title.innerText = slide_data.title[language]; + title_container.append(div_title); + }); + + const div_church_song_id = document.createElement("div"); + div_church_song_id.classList.add("song_id"); + + if (slide_data.church_song_id !== undefined) { + div_church_song_id.innerText = slide_data.church_song_id; + } + + div_content.append(div_church_song_id); + } break; + case "lyric": { + div_content.classList.add("lyric"); + + // add the individual text-lines + for (const line_package of slide_data.data) { + // create a template for the line + const line_template = document.createElement("div"); + line_template.classList.add("text_line"); + + // add the individual languages + languages.forEach((language, index) => { + const line = line_template.cloneNode(true) as HTMLDivElement; + line.classList.add(`language_${index}`); + line.innerText = line_package[language]; + + div_content.append(line); + }); + } + } break; + } + + return div_slide; +} + +function resize_slides_text(max_width: number, container_width: number) { + // change the font size by the size difference ratio + document.querySelectorAll("div.slide.lyric").forEach((ele) => { + ele.style.fontSize = `${container_width / max_width}em`; + }); +} \ No newline at end of file diff --git a/tsconfig_client.json b/tsconfig_client.json new file mode 100644 index 0000000..223aaf6 --- /dev/null +++ b/tsconfig_client.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "outDir": "client", + "removeComments": true, + "noImplicitAny": true + }, + "include": [ + "src\\client" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig_server.json similarity index 100% rename from tsconfig.json rename to tsconfig_server.json diff --git a/tsconfig_templates.json b/tsconfig_templates.json new file mode 100644 index 0000000..12cd147 --- /dev/null +++ b/tsconfig_templates.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2015", + "module": "ES2015", + "moduleResolution": "bundler", + "outDir": "casparcg-templates/JohnCG", + "removeComments": true + }, + "include": [ + "src\\templates" + ] +} \ No newline at end of file From 7496a5fc690908b0d1c0b551d5132a93167f0b29 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Thu, 22 Feb 2024 21:36:00 +0100 Subject: [PATCH 08/16] changed packaging to work with sharp --- README.md | 1 + build.ps1 | 36 +- client/logo.svg | 9 +- config.json | 2 +- package-lock.json | 2441 ++++++++++++++++++++++ src/server/SequenceItems/SequenceItem.ts | 36 +- yarn.lock | 1474 ------------- 7 files changed, 2486 insertions(+), 1513 deletions(-) create mode 100644 package-lock.json delete mode 100644 yarn.lock diff --git a/README.md b/README.md index cc740d8..a5d445d 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,4 @@ Generate lyric-graphics and play them out through CasparCG. - disable buttons, when no sequence is loaded - standardize all interfaces to snake_case - implement all countdown modes +- countdown: save in server wether it is finished \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 2d75962..2b609a0 100644 --- a/build.ps1 +++ b/build.ps1 @@ -2,33 +2,43 @@ $package_json = Get-Content -Raw package.json | ConvertFrom-Json $build_name = "JohnCG_" + $package_json.version +# name of the executabe +$node_exec_name = $build_name + ".exe" +$node_exec_name = "node.exe" # overwritten until there is a solution for packaging `sharp` + # clear the dist directory -Remove-Item -Path .\Dist\* -Recurse -New-Item -Type Directory .\Dist\build -New-Item -Type Directory .\Dist\$build_name +Remove-Item -Path .\dist -Recurse +New-Item -Type Directory .\dist\build +New-Item -Type Directory .\dist\$build_name # bundle the files -yarn run build-server -yarn run build-client -yarn run build-templates +npm run build-server +npm run build-client +npm run build-templates # create sea-prep.blob node --experimental-sea-config .\sea-config.json # get the node executable -node -e "require('fs').copyFileSync(process.execPath, 'dist/build/$build_name.exe')" - -# remove the signature from the node executable -& 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe' remove /s dist/build/$build_name.exe +node -e "require('fs').copyFileSync(process.execPath, 'dist/build/$node_exec_name')" -# modify the node executable -yarn postject dist/build/$build_name.exe NODE_SEA_BLOB dist/build/sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 +# disabled until there is a solution for packaging `sharp` +# # remove the signature from the node executable +# & 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe' remove /s dist/build/$node_exec_name +# # modify the node executable +# npx postject dist/build/$node_exec_name NODE_SEA_BLOB dist/build/sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 # copy the files in the output Copy-Item -Path .\config.json -Destination .\dist\$build_name -Exclude .eslintrc Copy-Item -Path .\casparcg-templates -Destination .\dist\$build_name -Exclude .eslintrc -Recurse Copy-Item -Path .\client -Destination .\dist\$build_name -Exclude .eslintrc -Recurse -Copy-Item -Path .\dist\build\$build_name.exe .\dist\$build_name -Recurse +Copy-Item -Path .\dist\build\$node_exec_name .\dist\$build_name -Recurse + +Copy-Item -Path .\node_modules\@img -Destination .\dist\$build_name\node_modules\@img\ -Recurse +Copy-Item -Path .\dist\build\main.js -Destination .\dist\$build_name\main.js + +# create a batch file, that starts node with the main.js +New-Item -Path .\dist\$build_name\$build_name.bat -Value "node.exe main.js`npause" # pack the files in a .tar.gz-file tar -cvzf .\dist\$build_name.tar.gz --directory=dist $build_name \ No newline at end of file diff --git a/client/logo.svg b/client/logo.svg index 4d0922c..9872a90 100644 --- a/client/logo.svg +++ b/client/logo.svg @@ -9,10 +9,15 @@ style="fill:rgb(224, 62, 62)" cx="256" cy="768" - r="256" /> + r="192" /> + r="192" /> + diff --git a/config.json b/config.json index bfa1fde..03227a3 100644 --- a/config.json +++ b/config.json @@ -4,7 +4,7 @@ }, "path": { "background_image": "C:/path/to/image/directory", - "song": "D:/Coding_z1glr/liedtexte_git" + "song": "D:/path/to/song/directory" }, "casparcg": { "templates": { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..98e9ee6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2441 @@ +{ + "name": "johncg", + "version": "ALPHA-1.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "johncg", + "version": "ALPHA-1.1.0", + "license": "MIT", + "dependencies": { + "casparcg-connection": "^6.2.1", + "iconv-lite": "^0.6.3", + "mime-types": "^2.1.35", + "osc": "^2.4.4", + "sharp": "^0.33.2", + "ws": "^8.16.0" + }, + "bin": { + "johncg": "out/main.js" + }, + "devDependencies": { + "@types/node": "^20.11.16", + "@types/ws": "^8.5.10", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "esbuild": "^0.20.0", + "eslint": "^8.56.0", + "husky": "^9.0.10", + "postject": "^1.0.0-alpha.6", + "typescript": "^5.3.3" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz", + "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.1" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz", + "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.1" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=11", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz", + "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=10.13", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz", + "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz", + "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz", + "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz", + "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz", + "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz", + "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz", + "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.1" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz", + "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.1" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz", + "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.1" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz", + "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.1" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz", + "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.1" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz", + "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.1" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz", + "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^0.45.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz", + "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz", + "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@serialport/binding-mock": { + "version": "10.2.2", + "license": "MIT", + "optional": true, + "dependencies": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@serialport/bindings-cpp": { + "version": "10.8.0", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "^10.2.1", + "debug": "^4.3.2", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=12.17.0 <13.0 || >=14.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-interface": { + "version": "1.2.2", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-packet-length": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "dependencies": { + "@serialport/parser-delimiter": "10.5.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-slip-encoder": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-spacepacket": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "debug": "^4.3.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.11.19", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/semver": { + "version": "7.5.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.11.3", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/casparcg-connection": { + "version": "6.2.1", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "tslib": "^2.5.0", + "xml2js": "^0.6.2" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.20.1", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/globals": { + "version": "13.24.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/husky": { + "version": "9.0.11", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "4.0.0", + "license": "Apache-2.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "license": "MIT", + "optional": true + }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/osc": { + "version": "2.4.4", + "license": "(MIT OR GPL-2.0)", + "dependencies": { + "long": "4.0.0", + "slip": "1.0.2", + "wolfy87-eventemitter": "5.2.9", + "ws": "8.13.0" + }, + "optionalDependencies": { + "serialport": "10.5.0" + } + }, + "node_modules/osc/node_modules/ws": { + "version": "8.13.0", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.3.0", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.6.0", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "license": "ISC" + }, + "node_modules/serialport": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "dependencies": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "10.8.0", + "@serialport/parser-byte-length": "10.5.0", + "@serialport/parser-cctalk": "10.5.0", + "@serialport/parser-delimiter": "10.5.0", + "@serialport/parser-inter-byte-timeout": "10.5.0", + "@serialport/parser-packet-length": "10.5.0", + "@serialport/parser-readline": "10.5.0", + "@serialport/parser-ready": "10.5.0", + "@serialport/parser-regex": "10.5.0", + "@serialport/parser-slip-encoder": "10.5.0", + "@serialport/parser-spacepacket": "10.5.0", + "@serialport/stream": "10.5.0", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/sharp": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", + "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "semver": "^7.5.4" + }, + "engines": { + "libvips": ">=8.15.1", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.2", + "@img/sharp-darwin-x64": "0.33.2", + "@img/sharp-libvips-darwin-arm64": "1.0.1", + "@img/sharp-libvips-darwin-x64": "1.0.1", + "@img/sharp-libvips-linux-arm": "1.0.1", + "@img/sharp-libvips-linux-arm64": "1.0.1", + "@img/sharp-libvips-linux-s390x": "1.0.1", + "@img/sharp-libvips-linux-x64": "1.0.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.1", + "@img/sharp-libvips-linuxmusl-x64": "1.0.1", + "@img/sharp-linux-arm": "0.33.2", + "@img/sharp-linux-arm64": "0.33.2", + "@img/sharp-linux-s390x": "0.33.2", + "@img/sharp-linux-x64": "0.33.2", + "@img/sharp-linuxmusl-arm64": "0.33.2", + "@img/sharp-linuxmusl-x64": "0.33.2", + "@img/sharp-wasm32": "0.33.2", + "@img/sharp-win32-ia32": "0.33.2", + "@img/sharp-win32-x64": "0.33.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slip": { + "version": "1.0.2", + "license": "(MIT OR GPL-2.0)" + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/wolfy87-eventemitter": { + "version": "5.2.9", + "license": "Unlicense" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.16.0", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index c2a9dcc..9905f00 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -99,7 +99,8 @@ export abstract class SequenceItemBase { } protected async load_background_images(image_path?: string, background_color?: string) { - let img_buffer, img_buffer_proxy; + let img_buffer; + let img_buffer_proxy; if (image_path !== undefined) { try { @@ -112,28 +113,32 @@ export abstract class SequenceItemBase { return; } - img_buffer_proxy = sharp(img_buffer).resize(240).toBuffer(); + img_buffer_proxy = await sharp(img_buffer).resize(240).toBuffer(); } // if the image_buffer is still undefined, try to use the backgroundColor if (background_color !== undefined) { - img_buffer = sharp({ + img_buffer = await sharp({ create: { width: 1, height: 1, channels: 4, - background: color_string_to_object(background_color) + background: background_color } }).png().toBuffer(); + // copy the the image to the proxy buffer, since only 1px anyway img_buffer_proxy = img_buffer; } - this.item_props.BackgroundImage = { - orig: `data:${mime.lookup(image_path)};base64,` + (img_buffer).toString("base64"), - proxy: `data:${mime.lookup(image_path)};base64,` + (await img_buffer_proxy).toString("base64") - }; + if (img_buffer !== undefined && img_buffer_proxy !== undefined) { + this.item_props.BackgroundImage = { + // if there is no image-path, the mime-type is PNG, since we created them from the background-color + orig: `data:${mime.lookup(image_path ?? ".png")};base64,` + (img_buffer).toString("base64"), + proxy: `data:${mime.lookup(image_path ?? ".png")};base64,` + (img_buffer_proxy).toString("base64") + }; + } } get props(): ItemProps { @@ -142,18 +147,3 @@ export abstract class SequenceItemBase { protected abstract get_background_image(proxy?: boolean): Promise; } - -function color_string_to_object(color: string): { r: number, g: number, b: number, alpha?: number} { - const regex_result = color.match(/#(?[0-9A-Fa-f]{2})(?[0-9A-Fa-f]{2})(?[0-9A-Fa-f]{2})(?[0-9A-Fa-f]{2})?/mg); - - if (!regex_result) { - throw new SyntaxError(`'${color}' is no valid color`); - } - - return { - r: Number(regex_result.groups?.r), - g: Number(regex_result.groups?.g), - b: Number(regex_result.groups?.b), - alpha: Number(regex_result.groups?.alpha) - }; -} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index b742e5a..0000000 --- a/yarn.lock +++ /dev/null @@ -1,1474 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@aashutoshrathi/word-wrap@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" - integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== - -"@emnapi/runtime@^0.45.0": - version "0.45.0" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-0.45.0.tgz#e754de04c683263f34fd0c7f32adfe718bbe4ddd" - integrity sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w== - dependencies: - tslib "^2.4.0" - -"@esbuild/aix-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" - integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== - -"@esbuild/android-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" - integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== - -"@esbuild/android-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" - integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== - -"@esbuild/android-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" - integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== - -"@esbuild/darwin-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" - integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== - -"@esbuild/darwin-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" - integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== - -"@esbuild/freebsd-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" - integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== - -"@esbuild/freebsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" - integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== - -"@esbuild/linux-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" - integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== - -"@esbuild/linux-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" - integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== - -"@esbuild/linux-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" - integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== - -"@esbuild/linux-loong64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" - integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== - -"@esbuild/linux-mips64el@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" - integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== - -"@esbuild/linux-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" - integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== - -"@esbuild/linux-riscv64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" - integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== - -"@esbuild/linux-s390x@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" - integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== - -"@esbuild/linux-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" - integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== - -"@esbuild/netbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" - integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== - -"@esbuild/openbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" - integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== - -"@esbuild/sunos-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" - integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== - -"@esbuild/win32-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" - integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== - -"@esbuild/win32-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" - integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== - -"@esbuild/win32-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" - integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== - -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== - dependencies: - eslint-visitor-keys "^3.3.0" - -"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" - integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== - -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@eslint/js@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" - integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== - -"@humanwhocodes/config-array@^0.11.13": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== - dependencies: - "@humanwhocodes/object-schema" "^2.0.2" - debug "^4.3.1" - minimatch "^3.0.5" - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/object-schema@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" - integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== - -"@img/sharp-darwin-arm64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz#0a52a82c2169112794dac2c71bfba9e90f7c5bd1" - integrity sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w== - optionalDependencies: - "@img/sharp-libvips-darwin-arm64" "1.0.1" - -"@img/sharp-darwin-x64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz#982e26bb9d38a81f75915c4032539aed621d1c21" - integrity sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg== - optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.0.1" - -"@img/sharp-libvips-darwin-arm64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz#81e83ffc2c497b3100e2f253766490f8fad479cd" - integrity sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw== - -"@img/sharp-libvips-darwin-x64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz#fc1fcd9d78a178819eefe2c1a1662067a83ab1d6" - integrity sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog== - -"@img/sharp-libvips-linux-arm64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz#26eb8c556a9b0db95f343fc444abc3effb67ebcf" - integrity sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA== - -"@img/sharp-libvips-linux-arm@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz#2a377b959ff7dd6528deee486c25461296a4fa8b" - integrity sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ== - -"@img/sharp-libvips-linux-s390x@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz#af28ac9ba929204467ecdf843330d791e9421e10" - integrity sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ== - -"@img/sharp-libvips-linux-x64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz#4273d182aa51912e655e1214ea47983d7c1f7f8d" - integrity sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw== - -"@img/sharp-libvips-linuxmusl-arm64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz#d150c92151cea2e8d120ad168b9c358d09c77ce8" - integrity sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg== - -"@img/sharp-libvips-linuxmusl-x64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz#e297c1a4252c670d93b0f9e51fca40a7a5b6acfd" - integrity sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw== - -"@img/sharp-linux-arm64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz#af3409f801a9bee1d11d0c7e971dcd6180f80022" - integrity sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew== - optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.0.1" - -"@img/sharp-linux-arm@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz#181f7466e6ac074042a38bfb679eb82505e17083" - integrity sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA== - optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.0.1" - -"@img/sharp-linux-s390x@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz#9c171f49211f96fba84410b3e237b301286fa00f" - integrity sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA== - optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.0.1" - -"@img/sharp-linux-x64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz#b956dfc092adc58c2bf0fae2077e6f01a8b2d5d7" - integrity sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A== - optionalDependencies: - "@img/sharp-libvips-linux-x64" "1.0.1" - -"@img/sharp-linuxmusl-arm64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz#10e0ec5a79d1234c6a71df44c9f3b0bef0bc0f15" - integrity sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.0.1" - -"@img/sharp-linuxmusl-x64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz#29e0030c24aa27c38201b1fc84e3d172899fcbe0" - integrity sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64" "1.0.1" - -"@img/sharp-wasm32@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz#38d7c740a22de83a60ad1e6bcfce17462b0d4230" - integrity sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ== - dependencies: - "@emnapi/runtime" "^0.45.0" - -"@img/sharp-win32-ia32@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz#09456314e223f68e5417c283b45c399635c16202" - integrity sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g== - -"@img/sharp-win32-x64@0.33.2": - version "0.33.2" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz#148e96dfd6e68747da41a311b9ee4559bb1b1471" - integrity sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@serialport/binding-mock@10.2.2": - version "10.2.2" - resolved "https://registry.yarnpkg.com/@serialport/binding-mock/-/binding-mock-10.2.2.tgz#d322a8116a97806addda13c62f50e73d16125874" - integrity sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw== - dependencies: - "@serialport/bindings-interface" "^1.2.1" - debug "^4.3.3" - -"@serialport/bindings-cpp@10.8.0": - version "10.8.0" - resolved "https://registry.yarnpkg.com/@serialport/bindings-cpp/-/bindings-cpp-10.8.0.tgz#79507b57022ac264e963e7fbf3647a3821569a20" - integrity sha512-OMQNJz5kJblbmZN5UgJXLwi2XNtVLxSKmq5VyWuXQVsUIJD4l9UGHnLPqM5LD9u3HPZgDI5w7iYN7gxkQNZJUw== - dependencies: - "@serialport/bindings-interface" "1.2.2" - "@serialport/parser-readline" "^10.2.1" - debug "^4.3.2" - node-addon-api "^5.0.0" - node-gyp-build "^4.3.0" - -"@serialport/bindings-interface@1.2.2", "@serialport/bindings-interface@^1.2.1": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz#c4ae9c1c85e26b02293f62f37435478d90baa460" - integrity sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA== - -"@serialport/parser-byte-length@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/parser-byte-length/-/parser-byte-length-10.5.0.tgz#f3d4c1c7923222df2f3d3c7c8aaaa207fe373b49" - integrity sha512-eHhr4lHKboq1OagyaXAqkemQ1XyoqbLQC8XJbvccm95o476TmEdW5d7AElwZV28kWprPW68ZXdGF2VXCkJgS2w== - -"@serialport/parser-cctalk@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/parser-cctalk/-/parser-cctalk-10.5.0.tgz#0ee88db0768a361b7cfb9a394b74e480c38e1992" - integrity sha512-Iwsdr03xmCKAiibLSr7b3w6ZUTBNiS+PwbDQXdKU/clutXjuoex83XvsOtYVcNZmwJlVNhAUbkG+FJzWwIa4DA== - -"@serialport/parser-delimiter@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/parser-delimiter/-/parser-delimiter-10.5.0.tgz#b0d93100cdfd0619d020a427d652495073f3b828" - integrity sha512-/uR/yT3jmrcwnl2FJU/2ySvwgo5+XpksDUR4NF/nwTS5i3CcuKS+FKi/tLzy1k8F+rCx5JzpiK+koqPqOUWArA== - -"@serialport/parser-inter-byte-timeout@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-10.5.0.tgz#8665ee5e6138f794ac055e83ef2d1c3653a577c0" - integrity sha512-WPvVlSx98HmmUF9jjK6y9mMp3Wnv6JQA0cUxLeZBgS74TibOuYG3fuUxUWGJALgAXotOYMxfXSezJ/vSnQrkhQ== - -"@serialport/parser-packet-length@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/parser-packet-length/-/parser-packet-length-10.5.0.tgz#4c4d733bdff8cc4749f2bd750e42e66f8f478def" - integrity sha512-jkpC/8w4/gUBRa2Teyn7URv1D7T//0lGj27/4u9AojpDVXsR6dtdcTG7b7dNirXDlOrSLvvN7aS5/GNaRlEByw== - -"@serialport/parser-readline@10.5.0", "@serialport/parser-readline@^10.2.1": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/parser-readline/-/parser-readline-10.5.0.tgz#df23365ae7f45679b1735deae26f72ba42802862" - integrity sha512-0aXJknodcl94W9zSjvU+sLdXiyEG2rqjQmvBWZCr8wJZjWEtv3RgrnYiWq4i2OTOyC8C/oPK8ZjpBjQptRsoJQ== - dependencies: - "@serialport/parser-delimiter" "10.5.0" - -"@serialport/parser-ready@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/parser-ready/-/parser-ready-10.5.0.tgz#1d9029f57b1abd664cb468e21bfccf7b44c6e8ea" - integrity sha512-QIf65LTvUoxqWWHBpgYOL+soldLIIyD1bwuWelukem2yDZVWwEjR288cLQ558BgYxH4U+jLAQahhqoyN1I7BaA== - -"@serialport/parser-regex@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/parser-regex/-/parser-regex-10.5.0.tgz#f98eab6e3d9bc99086269e9acf39a82db36d245f" - integrity sha512-9jnr9+PCxRoLjtGs7uxwsFqvho+rxuJlW6ZWSB7oqfzshEZWXtTJgJRgac/RuLft4hRlrmRz5XU40i3uoL4HKw== - -"@serialport/parser-slip-encoder@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/parser-slip-encoder/-/parser-slip-encoder-10.5.0.tgz#cb79ac0fda1fc87f049690ff7b498c787da67991" - integrity sha512-wP8m+uXQdkWSa//3n+VvfjLthlabwd9NiG6kegf0fYweLWio8j4pJRL7t9eTh2Lbc7zdxuO0r8ducFzO0m8CQw== - -"@serialport/parser-spacepacket@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/parser-spacepacket/-/parser-spacepacket-10.5.0.tgz#2fc077c0ec16a9532c511ad5f2ab12d588796bc7" - integrity sha512-BEZ/HAEMwOd8xfuJSeI/823IR/jtnThovh7ils90rXD4DPL1ZmrP4abAIEktwe42RobZjIPfA4PaVfyO0Fjfhg== - -"@serialport/stream@10.5.0": - version "10.5.0" - resolved "https://registry.yarnpkg.com/@serialport/stream/-/stream-10.5.0.tgz#cda8fb3e8d03094b0962a3d14b73adfcd591be58" - integrity sha512-gbcUdvq9Kyv2HsnywS7QjnEB28g+6OGB5Z8TLP7X+UPpoMIWoUsoQIq5Kt0ZTgMoWn3JGM2lqwTsSHF+1qhniA== - dependencies: - "@serialport/bindings-interface" "1.2.2" - debug "^4.3.2" - -"@types/json-schema@^7.0.12": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/node@*", "@types/node@^20.11.16": - version "20.11.16" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.16.tgz#4411f79411514eb8e2926f036c86c9f0e4ec6708" - integrity sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ== - dependencies: - undici-types "~5.26.4" - -"@types/semver@^7.5.0": - version "7.5.7" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.7.tgz#326f5fdda70d13580777bcaa1bc6fa772a5aef0e" - integrity sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg== - -"@types/ws@^8.5.10": - version "8.5.10" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" - integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== - dependencies: - "@types/node" "*" - -"@typescript-eslint/eslint-plugin@^6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" - integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== - dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/type-utils" "6.21.0" - "@typescript-eslint/utils" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - graphemer "^1.4.0" - ignore "^5.2.4" - natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/parser@^6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== - dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - -"@typescript-eslint/type-utils@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" - integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== - dependencies: - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/utils" "6.21.0" - debug "^4.3.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== - -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/utils@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" - integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - semver "^7.5.4" - -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== - dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" - -"@ungap/structured-clone@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn@^8.9.0: - version "8.11.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" - integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== - -ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -casparcg-connection@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/casparcg-connection/-/casparcg-connection-6.2.1.tgz#6b9c7ef49df1cb24cf64da6740e3d7b901eb2f74" - integrity sha512-2m+TWBile4qFvQtVmp/M20MegV4giwPs4jHfVBgj7LX2nWsbc0TX0Cz5pLAKeKnM/ErVKI8nR37Ye17zNXSIEA== - dependencies: - eventemitter3 "^5.0.1" - tslib "^2.5.0" - xml2js "^0.6.2" - -chalk@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@^1.0.0, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" - integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== - dependencies: - color-convert "^2.0.1" - color-string "^1.9.0" - -commander@^9.4.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" - integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -detect-libc@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" - integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -esbuild@^0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4" - integrity sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA== - optionalDependencies: - "@esbuild/aix-ppc64" "0.20.0" - "@esbuild/android-arm" "0.20.0" - "@esbuild/android-arm64" "0.20.0" - "@esbuild/android-x64" "0.20.0" - "@esbuild/darwin-arm64" "0.20.0" - "@esbuild/darwin-x64" "0.20.0" - "@esbuild/freebsd-arm64" "0.20.0" - "@esbuild/freebsd-x64" "0.20.0" - "@esbuild/linux-arm" "0.20.0" - "@esbuild/linux-arm64" "0.20.0" - "@esbuild/linux-ia32" "0.20.0" - "@esbuild/linux-loong64" "0.20.0" - "@esbuild/linux-mips64el" "0.20.0" - "@esbuild/linux-ppc64" "0.20.0" - "@esbuild/linux-riscv64" "0.20.0" - "@esbuild/linux-s390x" "0.20.0" - "@esbuild/linux-x64" "0.20.0" - "@esbuild/netbsd-x64" "0.20.0" - "@esbuild/openbsd-x64" "0.20.0" - "@esbuild/sunos-x64" "0.20.0" - "@esbuild/win32-arm64" "0.20.0" - "@esbuild/win32-ia32" "0.20.0" - "@esbuild/win32-x64" "0.20.0" - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - -eslint@^8.56.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" - integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.56.0" - "@humanwhocodes/config-array" "^0.11.13" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== - dependencies: - acorn "^8.9.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" - -esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -eventemitter3@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" - integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.2.9: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== - dependencies: - flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" - -flatted@^3.2.9: - version "3.2.9" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" - integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" - -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -husky@^9.0.10: - version "9.0.10" - resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.10.tgz#ddca8908deb5f244e9286865ebc80b54387672c2" - integrity sha512-TQGNknoiy6bURzIO77pPRu+XHi6zI7T93rX+QnJsoYFf3xdjKOur+IlfqzJGMHIK/wXrLg+GsvMs8Op7vI2jVA== - -iconv-lite@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -ignore@^5.2.0, ignore@^5.2.4: - version "5.3.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" - integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== - -import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -keyv@^4.5.3: - version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -long@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.35: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -node-addon-api@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" - integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== - -node-gyp-build@^4.3.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" - integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -optionator@^0.9.3: - version "0.9.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" - integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== - dependencies: - "@aashutoshrathi/word-wrap" "^1.2.3" - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - -osc@^2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/osc/-/osc-2.4.4.tgz#52b2e914253b04a792d28ee892b0fa1ca8bdc1a2" - integrity sha512-YJr2bUCQMc9BIaq1LXgqYpt5Ii7wNy2n0e0BkQiCSziMNrrsYHhH5OlExNBgCrQsum60EgXZ32lFsvR4aUf+ew== - dependencies: - long "4.0.0" - slip "1.0.2" - wolfy87-eventemitter "5.2.9" - ws "8.13.0" - optionalDependencies: - serialport "10.5.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -postject@^1.0.0-alpha.6: - version "1.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/postject/-/postject-1.0.0-alpha.6.tgz#9d022332272e2cfce8dea4cfce1ee6dd1b2ee135" - integrity sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A== - dependencies: - commander "^9.4.0" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -"safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sax@>=0.6.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" - integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== - -semver@^7.5.4: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== - dependencies: - lru-cache "^6.0.0" - -serialport@10.5.0: - version "10.5.0" - resolved "https://registry.yarnpkg.com/serialport/-/serialport-10.5.0.tgz#b85f614def6e8914e5865c798b0555330903a0f8" - integrity sha512-7OYLDsu5i6bbv3lU81pGy076xe0JwpK6b49G6RjNvGibstUqQkI+I3/X491yBGtf4gaqUdOgoU1/5KZ/XxL4dw== - dependencies: - "@serialport/binding-mock" "10.2.2" - "@serialport/bindings-cpp" "10.8.0" - "@serialport/parser-byte-length" "10.5.0" - "@serialport/parser-cctalk" "10.5.0" - "@serialport/parser-delimiter" "10.5.0" - "@serialport/parser-inter-byte-timeout" "10.5.0" - "@serialport/parser-packet-length" "10.5.0" - "@serialport/parser-readline" "10.5.0" - "@serialport/parser-ready" "10.5.0" - "@serialport/parser-regex" "10.5.0" - "@serialport/parser-slip-encoder" "10.5.0" - "@serialport/parser-spacepacket" "10.5.0" - "@serialport/stream" "10.5.0" - debug "^4.3.3" - -sharp@^0.33.2: - version "0.33.2" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.2.tgz#fcd52f2c70effa8a02160b1bfd989a3de55f2dfb" - integrity sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ== - dependencies: - color "^4.2.3" - detect-libc "^2.0.2" - semver "^7.5.4" - optionalDependencies: - "@img/sharp-darwin-arm64" "0.33.2" - "@img/sharp-darwin-x64" "0.33.2" - "@img/sharp-libvips-darwin-arm64" "1.0.1" - "@img/sharp-libvips-darwin-x64" "1.0.1" - "@img/sharp-libvips-linux-arm" "1.0.1" - "@img/sharp-libvips-linux-arm64" "1.0.1" - "@img/sharp-libvips-linux-s390x" "1.0.1" - "@img/sharp-libvips-linux-x64" "1.0.1" - "@img/sharp-libvips-linuxmusl-arm64" "1.0.1" - "@img/sharp-libvips-linuxmusl-x64" "1.0.1" - "@img/sharp-linux-arm" "0.33.2" - "@img/sharp-linux-arm64" "0.33.2" - "@img/sharp-linux-s390x" "0.33.2" - "@img/sharp-linux-x64" "0.33.2" - "@img/sharp-linuxmusl-arm64" "0.33.2" - "@img/sharp-linuxmusl-x64" "0.33.2" - "@img/sharp-wasm32" "0.33.2" - "@img/sharp-win32-ia32" "0.33.2" - "@img/sharp-win32-x64" "0.33.2" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slip@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/slip/-/slip-1.0.2.tgz#ba45a923034d6cf41b1a27aebe7128282c8d551f" - integrity sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g== - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -ts-api-utils@^1.0.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" - integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== - -tslib@^2.4.0, tslib@^2.5.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -typescript@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== - -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wolfy87-eventemitter@5.2.9: - version "5.2.9" - resolved "https://registry.yarnpkg.com/wolfy87-eventemitter/-/wolfy87-eventemitter-5.2.9.tgz#e879f770b30fbb6512a8afbb330c388591099c2a" - integrity sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw== - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -ws@8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== - -ws@^8.16.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" - integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== - -xml2js@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" - integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From eec688bdac0b4f21d570a6e02fb8e94d4c5c1f83 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Fri, 23 Feb 2024 14:42:03 +0100 Subject: [PATCH 09/16] added support for images and module-license-aggregation --- .gitignore | 4 +- README.md | 13 +- build.ps1 | 12 +- casparcg-templates/JohnCG/Countdown.html | 8 +- client/main.css | 9 +- license-generator.ts | 27 ++ license-reporter.config.ts | 14 + package-lock.json | 521 +++++++++++++++++++++-- package.json | 19 +- src/client/main.ts | 18 +- src/server/JGCPSendMessages.ts | 3 + src/server/Sequence.ts | 230 +++++++--- src/server/SequenceItems/Comment.ts | 6 +- src/server/SequenceItems/Countdown.ts | 24 +- src/server/SequenceItems/Image.ts | 100 +++++ src/server/SequenceItems/SequenceItem.ts | 37 +- src/server/SequenceItems/Song.ts | 6 +- src/server/config.ts | 14 +- 18 files changed, 891 insertions(+), 174 deletions(-) create mode 100644 license-generator.ts create mode 100644 license-reporter.config.ts create mode 100644 src/server/SequenceItems/Image.ts diff --git a/.gitignore b/.gitignore index b4b5ed6..e0a5ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ node_modules out dist client/*.js -casparcg-templates/JohnCG/*.js \ No newline at end of file +casparcg-templates/JohnCG/*.js +*.map +client/webfonts/bahnschrift.ttf \ No newline at end of file diff --git a/README.md b/README.md index a5d445d..4b09159 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,17 @@ Generate lyric-graphics and play them out through CasparCG. - try to get the template and client to use the settings file (CSS has default values, send song data overwrites them) - check client -> server slide_number out of range - client: check response-texts, if they are still correct -- create message log / show error messages -- write installation instruction +- write installation instruction (including Bahnschrift-Font installation) - client communication with osc over websocket? - add support for NodeCG -- look into document fragments - add CLI output to server - CasparCG: split text and image in two layers: enables text without background -- rewrite client in typescript - create dummy-sequence-items for unsupported ones - disable buttons, when no sequence is loaded -- standardize all interfaces to snake_case - implement all countdown modes -- countdown: save in server wether it is finished \ No newline at end of file +- countdown: save in server wether it is finished +- add option to change template-directory +- make sequence-comments another design in client +- client-messages: create message-log, group same +- repo-file-managment (move tsconfig files in respective directories?) +- build-script in node / integrate with license-generator \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 2b609a0..42f20ed 100644 --- a/build.ps1 +++ b/build.ps1 @@ -30,8 +30,8 @@ node -e "require('fs').copyFileSync(process.execPath, 'dist/build/$node_exec_nam # copy the files in the output Copy-Item -Path .\config.json -Destination .\dist\$build_name -Exclude .eslintrc -Copy-Item -Path .\casparcg-templates -Destination .\dist\$build_name -Exclude .eslintrc -Recurse -Copy-Item -Path .\client -Destination .\dist\$build_name -Exclude .eslintrc -Recurse +Copy-Item -Path .\casparcg-templates -Destination .\dist\$build_name -Exclude .eslintrc,*.map -Recurse +Copy-Item -Path .\client -Destination .\dist\$build_name -Exclude .eslintrc,*.map,bahnschrift.ttf -Recurse Copy-Item -Path .\dist\build\$node_exec_name .\dist\$build_name -Recurse Copy-Item -Path .\node_modules\@img -Destination .\dist\$build_name\node_modules\@img\ -Recurse @@ -40,5 +40,13 @@ Copy-Item -Path .\dist\build\main.js -Destination .\dist\$build_name\main.js # create a batch file, that starts node with the main.js New-Item -Path .\dist\$build_name\$build_name.bat -Value "node.exe main.js`npause" +# create and copy the licenses +npx esbuild license-generator.ts --platform=node --bundle --minify --outfile=.\dist\build\license-generator.js +npx license-reporter +node .\dist\build\license-generator.js + +Copy-Item -Path .\dist\build\licenses -Destination .\dist\$build_name\ -Recurse +# Copy-Item -Path .\LICENSE -Destination .\dist\$build_name\LICENSE + # pack the files in a .tar.gz-file tar -cvzf .\dist\$build_name.tar.gz --directory=dist $build_name \ No newline at end of file diff --git a/casparcg-templates/JohnCG/Countdown.html b/casparcg-templates/JohnCG/Countdown.html index 27dfeec..380b029 100644 --- a/casparcg-templates/JohnCG/Countdown.html +++ b/casparcg-templates/JohnCG/Countdown.html @@ -4,6 +4,12 @@ font-size: 0.125vw; } + * { + cursor: pointer; + + user-select: none; + } + body { margin: 0px; @@ -55,7 +61,7 @@ #main.stop { opacity: 0; - transition: opacity 1s; + transition: opacity 0.5s; } diff --git a/client/main.css b/client/main.css index ec6e4ef..6ab91c1 100644 --- a/client/main.css +++ b/client/main.css @@ -1,3 +1,8 @@ +@font-face { + font-family: 'Bahnschrift'; + src: url('/webfonts/bahnschrift.ttf'); +} + * { overflow: hidden; } @@ -5,7 +10,7 @@ body { background-color: rgb(31, 33, 42); - font-family: "Bahnschrift"; + font-family: Bahnschrift; color: white; margin: 0; @@ -246,6 +251,8 @@ div.button, div.button > * { background-repeat: no-repeat; background-position: center; background-size: cover; + + cursor: pointer; } .slide:hover { diff --git a/license-generator.ts b/license-generator.ts new file mode 100644 index 0000000..0dea5be --- /dev/null +++ b/license-generator.ts @@ -0,0 +1,27 @@ +import fs from "fs"; + +const packag = JSON.parse(fs.readFileSync("package.json", "utf-8")); + +const licenses_orig = JSON.parse(fs.readFileSync("dist/build/3rdpartylicenses.json", "utf-8")); + +const licenses = {}; + +licenses_orig.forEach((pack) => { + licenses[pack.name] = pack; +}); + +fs.mkdirSync("dist/build/licenses"); + +Object.keys(packag.dependencies).forEach((pack) => { + const lic = licenses[pack]; + + try { + fs.writeFileSync(`dist/build/licenses/${lic.name}.txt`, lic.licenseText, "utf-8"); + } catch (e) { + if (lic.licenseText === undefined) { + throw new EvalError(`ERROR: no license was found for the package '${lic.name}'`); + } + } +}); + +fs.copyFileSync("LICENSE", "dist/build/licenses/JohnCG.txt"); \ No newline at end of file diff --git a/license-reporter.config.ts b/license-reporter.config.ts new file mode 100644 index 0000000..14d9e72 --- /dev/null +++ b/license-reporter.config.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { IReporterConfiguration } from "@weichwarenprojekt/license-reporter"; + +export const configuration: Partial = { + defaultLicenseText: undefined, + output: "dist/build/3rdpartylicenses.json", // default: "./3rdpartylicenses.json" + overrides: [ + { + name: "osc", + licenseName: "MIT", + licenseText: "Copyright (c) 2011-2014 Colin Clark\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + } + ] +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 98e9ee6..e0762d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "casparcg-connection": "^6.2.1", + "fast-xml-parser": "^4.3.4", "iconv-lite": "^0.6.3", "mime-types": "^2.1.35", "osc": "^2.4.4", @@ -24,9 +25,9 @@ "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", + "@weichwarenprojekt/license-reporter": "^1.0.0", "esbuild": "^0.20.0", "eslint": "^8.56.0", - "husky": "^9.0.10", "postject": "^1.0.0-alpha.6", "typescript": "^5.3.3" } @@ -576,6 +577,50 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -608,6 +653,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@serialport/binding-mock": { "version": "10.2.2", "license": "MIT", @@ -1007,6 +1062,98 @@ "dev": true, "license": "ISC" }, + "node_modules/@weichwarenprojekt/license-reporter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@weichwarenprojekt/license-reporter/-/license-reporter-1.0.0.tgz", + "integrity": "sha512-f5RZXAQ7zz9FIUdxvuAQsB5a/axkuWuNK/zDWaZGe9YWgsxcM3RDEpsM3IXb2HPduJoZcQqlEBB5x+eKLJouqA==", + "dev": true, + "dependencies": { + "@weichwarenprojekt/ts-importer": "^0.1.7", + "chalk": "^4.1.2", + "commander": "^11.1.0", + "glob": "^10.3.10" + }, + "bin": { + "license-reporter": "dist/license-reporter.js" + } + }, + "node_modules/@weichwarenprojekt/license-reporter/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@weichwarenprojekt/license-reporter/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@weichwarenprojekt/license-reporter/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@weichwarenprojekt/license-reporter/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@weichwarenprojekt/ts-importer": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@weichwarenprojekt/ts-importer/-/ts-importer-0.1.7.tgz", + "integrity": "sha512-Y3OfCaZFaQ6bwD3xbbkmrcbHeHcEXFvzwzLD9GChUUbIOM2Hyv/IcgwHfacnwMWMn8+OjRmegmR/PAFr/7xorg==", + "dev": true, + "dependencies": { + "typescript": "^4.9.5" + } + }, + "node_modules/@weichwarenprojekt/ts-importer/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/acorn": { "version": "8.11.3", "dev": true, @@ -1184,6 +1331,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.3.4", "devOptional": true, @@ -1235,6 +1396,18 @@ "node": ">=6.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, "node_modules/esbuild": { "version": "0.20.1", "dev": true, @@ -1363,33 +1536,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/eslint/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/espree": { "version": "9.6.1", "dev": true, @@ -1489,6 +1635,27 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-parser": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.4.tgz", + "integrity": "sha512-utnwm92SyozgA3hhH2I8qldf2lBqm6qHOICawRNRFu1qMe3+oqr+GcXjGqTmXTMGE5T4eC03kr/rlh5C1IRdZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.17.1", "dev": true, @@ -1552,6 +1719,22 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "dev": true, @@ -1638,20 +1821,6 @@ "node": ">=8" } }, - "node_modules/husky": { - "version": "9.0.11", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.mjs" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "license": "MIT", @@ -1715,6 +1884,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "dev": true, @@ -1744,8 +1922,27 @@ }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, - "license": "ISC" + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } }, "node_modules/js-yaml": { "version": "4.1.0", @@ -1816,6 +2013,15 @@ "version": "4.0.0", "license": "Apache-2.0" }, + "node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/merge2": { "version": "1.4.1", "dev": true, @@ -1864,6 +2070,15 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.2", "devOptional": true, @@ -2008,6 +2223,22 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-type": { "version": "4.0.0", "dev": true, @@ -2249,6 +2480,18 @@ "node": ">=8" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -2269,6 +2512,71 @@ "version": "1.0.2", "license": "(MIT OR GPL-2.0)" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "dev": true, @@ -2280,6 +2588,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "dev": true, @@ -2291,6 +2612,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -2380,10 +2706,119 @@ "punycode": "^2.1.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wolfy87-eventemitter": { "version": "5.2.9", "license": "Unlicense" }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "dev": true, diff --git a/package.json b/package.json index 5ec62d6..55f30d7 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "license": "MIT", "dependencies": { "casparcg-connection": "^6.2.1", + "fast-xml-parser": "^4.3.4", "iconv-lite": "^0.6.3", "mime-types": "^2.1.35", "osc": "^2.4.4", @@ -17,22 +18,18 @@ "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", + "@weichwarenprojekt/license-reporter": "^1.0.0", "esbuild": "^0.20.0", "eslint": "^8.56.0", - "husky": "^9.0.10", "postject": "^1.0.0-alpha.6", "typescript": "^5.3.3" }, "scripts": { - "build-release": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./build.ps1", - - "build-server": "esbuild src/server/main.ts --tsconfig=tsconfig_server.json --bundle --platform=node --outfile=dist/build/main.js", - - "build-client": "esbuild src/client/main.ts --tsconfig=tsconfig_client.json --bundle --outfile=client/main.js", - "watch-client": "esbuild src/client/main.ts --tsconfig=tsconfig_client.json --bundle --watch --outfile=client/main.js", - - "build-templates": "esbuild src/templates/*.ts --target=chrome71 --tsconfig=tsconfig_templates.json --outdir=casparcg-templates/JohnCG/", - "watch-templates": "esbuild src/templates/*.ts --target=chrome71 --tsconfig=tsconfig_templates.json --watch --outdir=casparcg-templates/JohnCG/" - + "build-release": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./build.ps1", + "build-server": "esbuild src/server/main.ts --outfile=dist/build/main.js --tsconfig=tsconfig_server.json --platform=node --minify --bundle", + "build-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=tsconfig_client.json --bundle --minify", + "watch-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=tsconfig_client.json --bundle --sourcemap --watch", + "build-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=tsconfig_templates.json --target=chrome71 --minify", + "watch-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=tsconfig_templates.json --target=chrome71 --sourcemap --watch" } } diff --git a/src/client/main.ts b/src/client/main.ts index 1049fb7..e7e8f27 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -98,8 +98,6 @@ function display_items(data: ClientSequenceItems) { // display the visibility state display_visibility_state(data.metadata.visibility); - - msg_log.log("Sequence is loaded"); } function request_item_slides(item) { @@ -137,7 +135,10 @@ function display_item_slides(data: JGCPSend.ItemSlides) { part_arrays = create_song_slides(data as JGCPSend.SongSlides); break; case "Countdown": - part_arrays = create_countdown_slides(data as JGCPSend.CountdownSlides); + part_arrays = create_image_countdown_slides(data as JGCPSend.CountdownSlides); + break; + case "Image": + part_arrays = create_image_countdown_slides(data as JGCPSend.ImageSlides); break; default: console.error(`'${data.type}' is not supported`); @@ -148,8 +149,6 @@ function display_item_slides(data: JGCPSend.ItemSlides) { }); set_active_slide(data.client_id === client_id); - - msg_log.log("Item is loaded"); } function create_song_slides(data: JGCPSend.SongSlides): HTMLDivElement[] { @@ -207,7 +206,7 @@ function create_song_slides(data: JGCPSend.SongSlides): HTMLDivElement[] { return part_arrays; } -function create_countdown_slides(data: JGCPSend.CountdownSlides) { +function create_image_countdown_slides(data: JGCPSend.CountdownSlides | JGCPSend.ImageSlides): HTMLDivElement[] { // create the container for the part const div_slide_part = document.createElement("div"); div_slide_part.classList.add("slide_part"); @@ -244,6 +243,9 @@ function create_slide_object(data: ClientItemSlides, number: number) { case "Countdown": slide_object.data = "Templates/Countdown.html"; break; + case "Image": + slide_object.data = `${data.slides_template.background_image?.replace(/\\/g, "\\\\")}`; + break; } slide_object.classList.add("slide"); @@ -263,8 +265,10 @@ function create_slide_object(data: ClientItemSlides, number: number) { slide_object.contentWindow?.play(); + const container = slide_object.contentDocument ?? div_slide_container; + // register click event - slide_object.contentWindow?.addEventListener("click", () => { + container.addEventListener("click", () => { request_item_slide_select( Number(document.querySelector("div.sequence_item_container.selected")?.dataset.item_number), number diff --git a/src/server/JGCPSendMessages.ts b/src/server/JGCPSendMessages.ts index fcf50b8..5e9243c 100644 --- a/src/server/JGCPSendMessages.ts +++ b/src/server/JGCPSendMessages.ts @@ -1,6 +1,7 @@ import * as SequenceClass from "../server/Sequence"; import { ClientCountdownSlides } from "../server/SequenceItems/Countdown"; import { ClientSongSlides } from "../server/SequenceItems/Song"; +import { ClientImageSlides } from "./SequenceItems/Image"; /** * Base interface for sent JGCP-messages @@ -49,6 +50,8 @@ export type SongSlides = ItemSlidesBase & ClientSongSlides; export type CountdownSlides = ItemSlidesBase & ClientCountdownSlides; +export type ImageSlides = ItemSlidesBase & ClientImageSlides; + // temporary until full feature set export type NotImplementedSlides = ItemSlidesBase & { type: string; item: number; }; diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index febd788..d920bab 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -1,5 +1,7 @@ import path from "path"; -import { CasparCG } from "casparcg-connection"; +import { CasparCG, ClipInfo, Commands } from "casparcg-connection"; +import mime from "mime-types"; +import { XMLParser } from "fast-xml-parser"; import { SongElement } from "./SequenceItems/SongFile"; import { ClientItemSlides, ItemProps, ItemPropsBase, SequenceItem } from "./SequenceItems/SequenceItem"; @@ -7,9 +9,11 @@ import Song, { SongProps } from "./SequenceItems/Song"; import * as JGCPSend from "./JGCPSendMessages"; -import Config from "./config"; +import Config, { CasparCGConnectionSettings } from "./config"; import Countdown, { CountdownProps } from "./SequenceItems/Countdown"; import Comment, { CommentProps } from "./SequenceItems/Comment"; +import Image, { ImageProps } from "./SequenceItems/Image"; +import { TransitionType } from "casparcg-connection/dist/enums"; interface ClientSequenceItems { sequence_items: ItemProps[]; @@ -24,6 +28,23 @@ interface ActiveItemSlide { slide: number } +interface CasparCGPathsSettings { + /* eslint-disable @typescript-eslint/naming-convention */ + "data-path": string; + "initial-path": string; + "log-path": string; + "media-path": string; + "template-path": string; + /* eslint-enable @typescript-eslint/naming-convention */ +} + +interface CasparCGConnection { + connection: CasparCG, + settings: CasparCGConnectionSettings, + paths: CasparCGPathsSettings, + media: ClipInfo[] +} + class Sequence { // store the individual items of the sequence sequence_items: SequenceItem[] = []; @@ -32,31 +53,43 @@ class Sequence { private casparcg_visibility: boolean = Config.behaviour.show_on_load; - readonly casparcg_connections: CasparCG[] = []; + readonly casparcg_connections: CasparCGConnection[] = []; constructor(sequence: string) { this.parse_sequence(sequence); + const xml_parser = new XMLParser(); + // create the casparcg-connections - Config.casparcg.connections.forEach((connection_setting, index) => { - const casparcg_connection = new CasparCG({ + Config.casparcg.connections.forEach(async (connection_setting) => { + const connection = new CasparCG({ ...connection_setting, // eslint-disable-next-line @typescript-eslint/naming-convention autoConnect: true }); + const casparcg_connection = { + connection, + settings: connection_setting, + paths: xml_parser.parse((await (await connection.infoPaths()).request)?.data as string ?? "")?.paths, + media: (await (await connection.cls()).request)?.data ?? [] + }; + // add a listener to send send the current-slide on connection - casparcg_connection.addListener("connect", () => { + connection.addListener("connect", () => { // load the active-item - this.casparcg_load_item(this.active_item_number, index, false); + this.casparcg_load_item(casparcg_connection); }); + // clear the previous casparcg-output on the layers + this.casparcg_clear_layers(casparcg_connection); + + // load the first slide + this.casparcg_load_item(casparcg_connection); + // add the connection to the stored connections this.casparcg_connections.push(casparcg_connection); }); - - // clear the previous casparcg-output on the layers - this.casparcg_clear_layers(); this.set_active_item(0, 0); } @@ -65,26 +98,33 @@ class Sequence { this.casparcg_clear_layers(); this.casparcg_connections.forEach((casparcg_connection) => { - casparcg_connection.removeAllListeners(); - casparcg_connection.disconnect(); + casparcg_connection.connection.removeAllListeners(); + casparcg_connection.connection.disconnect(); }); } /** * clear the casparcg-layers used */ - casparcg_clear_layers() { - // setup auto-loading of the current-item on reconnection - this.casparcg_connections.forEach((casparcg_connection, index) => { - casparcg_connection.cgClear({ - channel: Config.casparcg.connections[index].channel, - layer: Config.casparcg.connections[index].layers[0] + casparcg_clear_layers(casparcg_connection?: CasparCGConnection) { + const clear_layers = (connection) => { + connection.connection.cgClear({ + channel: connection.settings.channel, + layer: connection.settings.layers[0] }); - casparcg_connection.cgClear({ - channel: Config.casparcg.connections[index].channel, - layer: Config.casparcg.connections[index].layers[1] + connection.connection.cgClear({ + channel: connection.settings.channel, + layer: connection.settings.layers[1] }); - }); + }; + + // if a conneciton was given as an argument, clear only it's layers + if (casparcg_connection !== undefined) { + clear_layers(casparcg_connection); + } else { + // clear the layers on all connnections + this.casparcg_connections.forEach(clear_layers); + } } parse_sequence(sequence: string): void { @@ -160,6 +200,9 @@ class Sequence { case "Countdown": this.sequence_items.push(new Countdown(item_data as CountdownProps)); break; + case "Image": + this.sequence_items.push(new Image(item_data as ImageProps)); + break; default: // if it wasn't caught by other cases, it is either a comment or not implemented yet -> if there is no file specified, treat it as comment if (!Object.keys(item_data).includes("FileName")) { @@ -193,7 +236,7 @@ class Sequence { this.active_sequence_item.set_active_slide(slide); - this.casparcg_load_item(this.active_item); + this.casparcg_load_item(); return this.active_item_slide; } @@ -299,57 +342,110 @@ class Sequence { return item; } - private casparcg_load_item(item: number, index?: number, flip_layer: boolean = true): void { - if (flip_layer) { + private casparcg_load_item(casparcg_connection?: CasparCGConnection): void { + const connections = casparcg_connection ? [casparcg_connection] : this.casparcg_connections; + + // if no connection was give, flip the layers + if (casparcg_connection === undefined) { // clear the lower layer - this.casparcg_connections.forEach((casparcg_connection, loop_index) => { - casparcg_connection.cgClear({ - channel: Config.casparcg.connections[loop_index].channel, - layer: Config.casparcg.connections[loop_index].layers[0] + connections.forEach((casparcg_connection) => { + casparcg_connection.connection.cgClear({ + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers[0] }); - }); - // swap the higer layer to the lower layer - this.casparcg_connections.forEach((casparcg_connection, loop_index) => { - casparcg_connection.swap({ - channel: Config.casparcg.connections[loop_index].channel, - layer: Config.casparcg.connections[loop_index].layers[0], - channel2: Config.casparcg.connections[loop_index].channel, - layer2: Config.casparcg.connections[loop_index].layers[1], + casparcg_connection.connection.swap({ + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers[0], + channel2: casparcg_connection.settings.channel, + layer2: casparcg_connection.settings.layers[1], transforms: true }); }); } - this.casparcg_connections.forEach(async (casparcg_connection, loop_index) => { - // load the item into casparcg - casparcg_connection.cgAdd({ - /* eslint-disable @typescript-eslint/naming-convention */ - channel: Config.casparcg.connections[loop_index].channel, - layer: Config.casparcg.connections[loop_index].layers[1], - cgLayer: 0, - playOnLoad: this.casparcg_visibility, - template: Config.casparcg.templates[this.active_sequence_item.props.type], - // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug - data: JSON.stringify(JSON.stringify(await this.active_sequence_item.create_render_object(), (_key, val) => { - if (typeof val === "string") { - return val.replace("\"", "\\u0022"); + connections.forEach(async (casparcg_connection) => { + // generate the render-object + const render_object = await this.active_sequence_item.create_render_object(); + + // depending on the type, use different caspar-cg-functions + switch (render_object?.caspar_type) { + case "template": + casparcg_connection.connection.cgAdd({ + /* eslint-disable @typescript-eslint/naming-convention */ + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers[1], + cgLayer: 0, + playOnLoad: this.casparcg_visibility, + template: Config.casparcg.templates[this.active_sequence_item.props.type], + // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug + data: JSON.stringify(JSON.stringify(render_object, (_key, val) => { + if (typeof val === "string") { + return val.replace("\"", "\\u0022"); + } else { + return val; + } + })) + /* eslint-enable @typescript-eslint/naming-convention */ + }); + break; + case "media": { + // make it all uppercase and remove the extension to match casparcg-clips and make sure it uses forward slashes + const req_name = render_object.file_name.replace(/\.[^(\\.]+$/, "").toUpperCase().replace(/\\/g, "/"); + + let media_result: ClipInfo | undefined; + + // check all the casparcg-files, wether they contain a media-file that matches the path + for (const m of casparcg_connection.media) { + const media_file = m.clip.toUpperCase().replace(/\\/, "/"); + + if (req_name.endsWith(media_file)) { + media_result = m; + break; + } + } + + // if a matching media-file was found, use it + if (media_result !== undefined) { + casparcg_connection.connection.play({ + /* eslint-disable @typescript-eslint/naming-convention */ + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers[1], + clip: media_result.clip, + transition: { + duration: 12.5, // 25 frames = 1s@25fps + transitionType: TransitionType.Mix + } + /* eslint-enable @typescript-eslint/naming-convention */ + }); } else { - return val; + casparcg_connection.connection.executeCommand({ + /* eslint-disable @typescript-eslint/naming-convention */ + command: Commands.PlayHtml, + params: { + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers[1], + url: JSON.stringify(render_object.background_image), + transition: { + duration: 12.5, // 25 frames = 1s@25fps + transitionType: TransitionType.Mix + } + /* eslint-enable @typescript-eslint/naming-convention */ + } + }); } - })) - /* eslint-enable @typescript-eslint/naming-convention */ - }); + } break; + } }); } private casparcg_select_slide(slide: number): void { - this.casparcg_connections.forEach((casparcg_connection, loop_index) => { + this.casparcg_connections.forEach((casparcg_connection) => { // jump to the slide-number in casparcg - casparcg_connection.cgInvoke({ + casparcg_connection.connection.cgInvoke({ /* eslint-disable @typescript-eslint/naming-convention */ - channel: Config.casparcg.connections[loop_index].channel, - layer: Config.casparcg.connections[loop_index].layers[1], + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers[1], cgLayer: 0, method: `jump(${slide})` /* eslint-enable @typescript-eslint/naming-convention */ @@ -358,17 +454,17 @@ class Sequence { } set_visibility(visibility: boolean): void { - this.casparcg_connections.forEach((casparcg_connection, loop_index) => { + this.casparcg_connections.forEach((casparcg_connection) => { // clear the background-layer, so that the foreground has an hide-animation - casparcg_connection.clear({ - channel: Config.casparcg.connections[loop_index].channel, - layer: Config.casparcg.connections[loop_index].layers[0] + casparcg_connection.connection.clear({ + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers[0] }); const options = { /* eslint-disable @typescript-eslint/naming-convention */ - channel: Config.casparcg.connections[loop_index].channel, - layer: Config.casparcg.connections[loop_index].layers[1], + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers[1], cgLayer: 0 /* eslint-enable @typescript-eslint/naming-convention */ }; @@ -376,9 +472,9 @@ class Sequence { this.casparcg_visibility = visibility; if (visibility) { - casparcg_connection.cgPlay(options); + casparcg_connection.connection.cgPlay(options); } else { - casparcg_connection.cgStop(options); + casparcg_connection.connection.cgStop(options); } }); } @@ -438,6 +534,8 @@ function parse_item_value_string(key: string, value: string): { [P in keyof Item // assume the type from the file-extension if (path.extname(value) === ".sng") { return_props.type = "Song"; + } else if (mime.lookup(value).split("/", 1)[0] === "image") { + return_props.type = "Image"; } return_props[key] = value; break; diff --git a/src/server/SequenceItems/Comment.ts b/src/server/SequenceItems/Comment.ts index f627cd3..acaa04d 100644 --- a/src/server/SequenceItems/Comment.ts +++ b/src/server/SequenceItems/Comment.ts @@ -1,20 +1,23 @@ import { ClientItemSlidesBase, ItemProps, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; export interface CommentProps extends ItemPropsBase { + type: "Comment"; selectable: false; } export interface ClientCommentSlides extends ClientItemSlidesBase { type: "Comment"; + slides_template: CommentRenderObject & { mute_transition: true; }; } export interface CommentRenderObject extends ItemRenderObjectBase { + caspar_type: "media"; } export default class Comment extends SequenceItemBase { protected item_props: CommentProps; - protected slide_count: number; + protected slide_count: number = 0; constructor(props: CommentProps) { super(); @@ -31,6 +34,7 @@ export default class Comment extends SequenceItemBase { item: this.props.item, slides: [], slides_template: { + caspar_type: "media", slides: [], slide: 0, mute_transition: true diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts index a9558bc..d5ee286 100644 --- a/src/server/SequenceItems/Countdown.ts +++ b/src/server/SequenceItems/Countdown.ts @@ -1,4 +1,6 @@ +import path from "path"; import { convert_color_to_hex } from "../Sequence"; +import Config from "../config"; import { ClientItemSlidesBase, DeepPartial, FontFormat, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; const countdown_mode_items = ["duration", "end_time", "stopwatch", "clock"]; @@ -19,10 +21,9 @@ export interface CountdownProps extends CountdownSequenceItemProps { position: CountdownPosition; mode: CountdownMode; show_seconds: boolean; - background: { - path?: string; - color?: string; - } + // eslint-disable-next-line @typescript-eslint/naming-convention + fileName?: string; + background_color?: string; } export interface ClientCountdownSlides extends ClientItemSlidesBase { @@ -35,6 +36,8 @@ export interface ClientCountdownSlides extends ClientItemSlidesBase { } export interface CountdownRenderObject extends ItemRenderObjectBase { + type: "Countdown"; + caspar_type: "template"; slides: []; position: CountdownPosition; font_format: FontFormat; @@ -81,10 +84,9 @@ export default class Countdown extends SequenceItemBase { show_seconds: hex_data.show_seconds, font_format: hex_data.font_format, mode: hex_data.mode, - background: { - path: hex_data.background_image, - color: hex_data.background_color - } + // eslint-disable-next-line @typescript-eslint/naming-convention + fileName: hex_data.background_image, + background_color: hex_data.background_color }; } @@ -132,6 +134,8 @@ export default class Countdown extends SequenceItemBase { async create_render_object(proxy?: boolean): Promise { return { + caspar_type: "template", + type: "Countdown", background_image: await this.get_background_image(proxy), slide: 0, slides: [], @@ -145,7 +149,9 @@ export default class Countdown extends SequenceItemBase { protected async get_background_image(proxy?: boolean): Promise { // check wether the images have yet been laoded if (this.props.BackgroundImage === undefined) { - await this.load_background_images(this.props.background.path, this.props.background.color); + const image_path = path.join(Config.path.background_image, this.props.fileName ?? ""); + + await this.load_background_images(image_path, this.props.background_color); } return this.props.BackgroundImage![proxy ? "proxy" : "orig"]; diff --git a/src/server/SequenceItems/Image.ts b/src/server/SequenceItems/Image.ts new file mode 100644 index 0000000..9522486 --- /dev/null +++ b/src/server/SequenceItems/Image.ts @@ -0,0 +1,100 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import path from "path"; +import { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; +import Config from "../config"; + +export interface ImageProps extends ItemPropsBase { + /* eslint-disable @typescript-eslint/naming-convention */ + type: "Image"; + FileName: string; + /* eslint-enable @typescript-eslint/naming-convention */ +} + +export interface ImageRenderObject extends ItemRenderObjectBase { + caspar_type: "media"; + file_name: string; + slides: []; +} + +export interface ClientImageSlides extends ClientItemSlidesBase { + type: "Image"; + slides: [], + slides_template: ImageRenderObject & { mute_transition: true; }; +} + +export default class Image extends SequenceItemBase { + protected item_props: ImageProps; + + protected slide_count: number = 1; + + constructor(props: ImageProps) { + super(); + + this.item_props = props; + } + + set_active_slide(slide?: number): number { + slide = this.validate_slide_number(slide); + + return slide; + } + + async create_client_object_item_slides(): Promise { + return { + title: this.props.FileName, + type: "Image", + item: this.props.item, + slides: [], + slides_template: { + ...await this.create_render_object(true), + mute_transition: true + } + }; + } + + async create_render_object(proxy?: boolean): Promise { + return { + caspar_type: "media", + slide: 0, + slides: [], + file_name: this.props.FileName, + background_image: await this.get_background_image(proxy), + mute_transition: true + }; + } + + navigate_slide(steps: number): number { + if (typeof steps !== "number") { + throw new TypeError(`steps ('${steps}') is no number`); + } + + if (![-1, 1].includes(steps)) { + throw new RangeError(`steps must be -1 or 1, but is ${steps}`); + } + + // directly return the steps as item-navigation-steps, since this can't be navigated + return steps; + } + + protected async get_background_image(proxy?: boolean): Promise { + // check wether the images have yet been laoded + if (this.props.BackgroundImage === undefined) { + // if the filename is absolute, use it as an filename, if not resolve it relative to the song-path (because Songbeamer) + const filename = path.isAbsolute(this.props.FileName) ? + this.props.FileName + : path.resolve(Config.path.song, this.props.FileName); + + await this.load_background_images(filename); + } + + return this.props.BackgroundImage![proxy ? "proxy" : "orig"]; + } + + get active_slide(): number { + return 0; + } + + get props(): ImageProps { + return this.item_props; + } +} \ No newline at end of file diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index 9905f00..114f44d 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -1,14 +1,13 @@ import { promises as fs } from "fs"; -import path from "path"; import mime from "mime-types"; import sharp from "sharp"; -import Config from "../config"; import Song, { ClientSongSlides, SongProps, SongRenderObject } from "./Song"; import Countdown, { ClientCountdownSlides, CountdownProps, CountdownRenderObject } from "./Countdown"; import Comment, { ClientCommentSlides, CommentProps, CommentRenderObject } from "./Comment"; +import Image, { ClientImageSlides, ImageProps, ImageRenderObject } from "./Image"; -export type SequenceItem = Song | Countdown | Comment; +export type SequenceItem = Song | Countdown | Comment | Image; export type DeepPartial = { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; @@ -29,10 +28,13 @@ export interface ItemPropsBase { /* eslint-enable @typescript-eslint/naming-convention */ } -export type ItemProps = SongProps | CountdownProps | CommentProps; +export type ItemProps = SongProps | CountdownProps | CommentProps | ImageProps; + +type CasparGeneratorType = "media" | "template"; // interface for a renderer-object export interface ItemRenderObjectBase { + caspar_type: CasparGeneratorType; slides: Array; slide: number; background_image?: string; @@ -40,7 +42,7 @@ export interface ItemRenderObjectBase { mute_transition?: boolean; } -export type ItemRenderObject = SongRenderObject | CountdownRenderObject | CommentRenderObject; +export type ItemRenderObject = SongRenderObject | CountdownRenderObject | CommentRenderObject | ImageRenderObject; export interface ClientItemSlidesBase { type: string; @@ -50,7 +52,7 @@ export interface ClientItemSlidesBase { slides_template: ItemRenderObject & { mute_transition: true; }; } -export type ClientItemSlides = ClientSongSlides | ClientCountdownSlides | ClientCommentSlides; +export type ClientItemSlides = ClientSongSlides | ClientCountdownSlides | ClientCommentSlides | ClientImageSlides; export interface FontFormat { /* eslint-disable @typescript-eslint/naming-convention */ @@ -99,25 +101,21 @@ export abstract class SequenceItemBase { } protected async load_background_images(image_path?: string, background_color?: string) { - let img_buffer; - let img_buffer_proxy; + let img_buffer: Buffer | undefined = undefined; + let img_buffer_proxy: Buffer | undefined = undefined; if (image_path !== undefined) { try { - img_buffer = await fs.readFile(path.join(Config.path.background_image, image_path)); + img_buffer = await fs.readFile(image_path); + + img_buffer_proxy = await sharp(img_buffer).resize(240).toBuffer(); } catch (e) { - this.item_props.BackgroundImage = { - orig: "", - proxy: "" - }; - - return; + ""; } - img_buffer_proxy = await sharp(img_buffer).resize(240).toBuffer(); } // if the image_buffer is still undefined, try to use the backgroundColor - if (background_color !== undefined) { + if (img_buffer === undefined && background_color !== undefined) { img_buffer = await sharp({ create: { width: 1, @@ -126,8 +124,7 @@ export abstract class SequenceItemBase { background: background_color } }).png().toBuffer(); - - + // copy the the image to the proxy buffer, since only 1px anyway img_buffer_proxy = img_buffer; } @@ -138,6 +135,8 @@ export abstract class SequenceItemBase { orig: `data:${mime.lookup(image_path ?? ".png")};base64,` + (img_buffer).toString("base64"), proxy: `data:${mime.lookup(image_path ?? ".png")};base64,` + (img_buffer_proxy).toString("base64") }; + } else { + this.item_props.BackgroundImage = { orig: "", proxy: "" }; } } diff --git a/src/server/SequenceItems/Song.ts b/src/server/SequenceItems/Song.ts index e498a31..4ac5750 100644 --- a/src/server/SequenceItems/Song.ts +++ b/src/server/SequenceItems/Song.ts @@ -25,6 +25,7 @@ export interface LyricSlide { export type ItemSlide = LyricSlide | TitleSlide; export interface SongRenderObject extends ItemRenderObjectBase { + caspar_type: "template"; type: "Song"; slides: ItemSlide[]; languages: number[]; @@ -114,6 +115,7 @@ export default class Song extends SequenceItemBase { const return_object: SongRenderObject = { type: "Song", + caspar_type: "template", slides: [ this.song_file.part_title ], @@ -223,7 +225,9 @@ export default class Song extends SequenceItemBase { protected async get_background_image(proxy?: boolean): Promise { // check wether the images have yet been laoded if (this.props.BackgroundImage === undefined) { - await this.load_background_images(this.song_file.metadata.BackgroundImage); + const image_path = path.join(Config.path.background_image, this.song_file.metadata.BackgroundImage ?? ""); + + await this.load_background_images(image_path); } return this.props.BackgroundImage![proxy ? "proxy" : "orig"]; diff --git a/src/server/config.ts b/src/server/config.ts index f1cd179..8aa04c3 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -1,3 +1,10 @@ +export interface CasparCGConnectionSettings { + host: string; + port: number; + channel: number; + layers: [number, number]; +} + interface ConfigJSON { behaviour: { show_on_load: boolean; @@ -13,12 +20,7 @@ interface ConfigJSON { Countdown: string /* eslint-enable @typescript-eslint/naming-convention */ }; - connections: { - host: string; - port: number; - channel: number; - layers: [number, number]; - }[]; + connections: CasparCGConnectionSettings[]; }; client_server: { http: { From 58685a923ca84a6256dc39d0bdddca55d1496581 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Fri, 23 Feb 2024 21:11:35 +0100 Subject: [PATCH 10/16] applied eslinte-rules forbidding any --- .eslintignore | 5 +- .eslintrc | 10 +- .vscode/tasks.json | 22 +- README.md | 3 +- license-reporter.config.ts | 3 +- package-lock.json | 286 +++--------------- package.json | 5 +- src/client/main.ts | 52 ++-- .../client/tsconfig.json | 7 +- src/server/JGCPReceiveMessages.ts | 17 +- src/server/JGCPSendMessages.ts | 6 - src/server/Sequence.ts | 70 +++-- src/server/SequenceItems/Comment.ts | 34 ++- src/server/SequenceItems/Countdown.ts | 23 +- src/server/SequenceItems/Image.ts | 3 +- src/server/SequenceItems/SequenceItem.ts | 2 +- src/server/SequenceItems/Song.ts | 7 +- src/server/SequenceItems/SongFile.ts | 4 +- src/server/config.ts | 2 +- src/server/control.ts | 17 +- src/server/servers/http-server.ts | 6 +- src/server/servers/osc-server.ts | 57 ++-- src/server/servers/websocket-server.ts | 10 +- .../server/tsconfig.json | 10 +- src/templates/Countdown.ts | 17 +- src/templates/Song.ts | 2 +- .../templates/tsconfig.json | 5 +- 27 files changed, 245 insertions(+), 440 deletions(-) rename tsconfig_client.json => src/client/tsconfig.json (72%) rename tsconfig_server.json => src/server/tsconfig.json (53%) rename tsconfig_templates.json => src/templates/tsconfig.json (80%) diff --git a/.eslintignore b/.eslintignore index 733481e..8cbc828 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,6 @@ node_modules out -dist \ No newline at end of file +dist +casparcg-templates +license-generator.ts +license-reporter.config.ts \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 9475adf..7c55373 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,10 +4,18 @@ "plugins": [ "@typescript-eslint" ], + "parserOptions": { + "project": [ + "src/server/tsconfig.json", + "src/client/tsconfig.json", + "src/templates/tsconfig.json" + ] + }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-type-checked" ], "rules": { "semi": ["error", "always"], diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f3b4fdf..ec87393 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,32 +10,12 @@ }, { "type": "typescript", - "tsconfig": "tsconfig_server.json", + "tsconfig": "src/server/tsconfig.json", "problemMatcher": [ "$tsc" ], "group": "build", "label": "tsc: build - server" - }, - { - "type": "typescript", - "tsconfig": "tsconfig_client.json", - "option": "watch", - "problemMatcher": [ - "$tsc-watch" - ], - "group": "build", - "label": "tsc: watch - tsconfig_client.json" - }, - { - "type": "typescript", - "tsconfig": "tsconfig_templates.json", - "option": "watch", - "problemMatcher": [ - "$tsc-watch" - ], - "group": "build", - "label": "tsc: watch - tsconfig_templates.json" } ] } \ No newline at end of file diff --git a/README.md b/README.md index 4b09159..708a694 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,5 @@ Generate lyric-graphics and play them out through CasparCG. - make sequence-comments another design in client - client-messages: create message-log, group same - repo-file-managment (move tsconfig files in respective directories?) -- build-script in node / integrate with license-generator \ No newline at end of file +- build-script in node / integrate with license-generator +- media-to-media-switch: there is no switching to background-layer \ No newline at end of file diff --git a/license-reporter.config.ts b/license-reporter.config.ts index 14d9e72..16698ac 100644 --- a/license-reporter.config.ts +++ b/license-reporter.config.ts @@ -1,9 +1,8 @@ -/* eslint-disable @typescript-eslint/naming-convention */ import { IReporterConfiguration } from "@weichwarenprojekt/license-reporter"; export const configuration: Partial = { defaultLicenseText: undefined, - output: "dist/build/3rdpartylicenses.json", // default: "./3rdpartylicenses.json" + output: "dist/build/3rdpartylicenses.json", overrides: [ { name: "osc", diff --git a/package-lock.json b/package-lock.json index e0762d2..ce51b88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "fast-xml-parser": "^4.3.4", "iconv-lite": "^0.6.3", "mime-types": "^2.1.35", - "osc": "^2.4.4", + "node-osc": "^9.1.0", "sharp": "^0.33.2", "ws": "^8.16.0" }, @@ -21,7 +21,9 @@ "johncg": "out/main.js" }, "devDependencies": { + "@types/mime-types": "^2.1.4", "@types/node": "^20.11.16", + "@types/node-osc": "^6.0.3", "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", @@ -663,175 +665,17 @@ "node": ">=14" } }, - "node_modules/@serialport/binding-mock": { - "version": "10.2.2", - "license": "MIT", - "optional": true, - "dependencies": { - "@serialport/bindings-interface": "^1.2.1", - "debug": "^4.3.3" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@serialport/bindings-cpp": { - "version": "10.8.0", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@serialport/bindings-interface": "1.2.2", - "@serialport/parser-readline": "^10.2.1", - "debug": "^4.3.2", - "node-addon-api": "^5.0.0", - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=12.17.0 <13.0 || >=14.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/bindings-interface": { - "version": "1.2.2", - "license": "MIT", - "optional": true, - "engines": { - "node": "^12.22 || ^14.13 || >=16" - } - }, - "node_modules/@serialport/parser-byte-length": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/parser-cctalk": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/parser-delimiter": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/parser-inter-byte-timeout": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/parser-packet-length": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/@serialport/parser-readline": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "dependencies": { - "@serialport/parser-delimiter": "10.5.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/parser-ready": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/parser-regex": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/parser-slip-encoder": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/parser-spacepacket": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/stream": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "dependencies": { - "@serialport/bindings-interface": "1.2.2", - "debug": "^4.3.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "dev": true, "license": "MIT" }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true + }, "node_modules/@types/node": { "version": "20.11.19", "dev": true, @@ -840,6 +684,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-osc": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/node-osc/-/node-osc-6.0.3.tgz", + "integrity": "sha512-f0JUTDVAlk/mV9RH6jE6g/8Y7l+Mvi3f1x7xv0RG3VI1PktaXEd60mR+4plpMS9VbRovFoPsS7qaaIDhFXdaEw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.7", "dev": true, @@ -1228,6 +1081,11 @@ "dev": true, "license": "MIT" }, + "node_modules/binpack": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/binpack/-/binpack-0.1.0.tgz", + "integrity": "sha512-KcSrsGiIKgklTWweVb9XnZPWO1/rGSsK3fwR7VnbDPbLKPlkvSKd/ZrJ1W712r6HzH5u0fa/AZCftATO09x8Aw==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "dev": true, @@ -1347,7 +1205,7 @@ }, "node_modules/debug": { "version": "4.3.4", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -2009,10 +1867,6 @@ "dev": true, "license": "MIT" }, - "node_modules/long": { - "version": "4.0.0", - "license": "Apache-2.0" - }, "node_modules/lru-cache": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", @@ -2081,7 +1935,7 @@ }, "node_modules/ms": { "version": "2.1.2", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -2089,19 +1943,15 @@ "dev": true, "license": "MIT" }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "license": "MIT", - "optional": true - }, - "node_modules/node-gyp-build": { - "version": "4.8.0", - "license": "MIT", - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "node_modules/node-osc": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/node-osc/-/node-osc-9.1.0.tgz", + "integrity": "sha512-7Ey92VAnGAkQI0MfxFH365v6+wUyHU/EVt5/v4Q81w8b+F/pVQxvmEzPnp70IxvDipSgYtSGer/iZUHZeKCzwg==", + "dependencies": { + "osc-min": "^1.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/once": { @@ -2128,36 +1978,15 @@ "node": ">= 0.8.0" } }, - "node_modules/osc": { - "version": "2.4.4", - "license": "(MIT OR GPL-2.0)", + "node_modules/osc-min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/osc-min/-/osc-min-1.1.2.tgz", + "integrity": "sha512-8DbiO8ME85R75stgNVCZtHxB9MNBBNcyy+isNBXrsFeinXGjwNAauvKVmGlfRas5VJWC/mhzIx7spR2gFvWxvg==", "dependencies": { - "long": "4.0.0", - "slip": "1.0.2", - "wolfy87-eventemitter": "5.2.9", - "ws": "8.13.0" + "binpack": "~0" }, - "optionalDependencies": { - "serialport": "10.5.0" - } - }, - "node_modules/osc/node_modules/ws": { - "version": "8.13.0", - "license": "MIT", "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node": ">=0.10.0" } }, "node_modules/p-limit": { @@ -2395,33 +2224,6 @@ "version": "4.0.0", "license": "ISC" }, - "node_modules/serialport": { - "version": "10.5.0", - "license": "MIT", - "optional": true, - "dependencies": { - "@serialport/binding-mock": "10.2.2", - "@serialport/bindings-cpp": "10.8.0", - "@serialport/parser-byte-length": "10.5.0", - "@serialport/parser-cctalk": "10.5.0", - "@serialport/parser-delimiter": "10.5.0", - "@serialport/parser-inter-byte-timeout": "10.5.0", - "@serialport/parser-packet-length": "10.5.0", - "@serialport/parser-readline": "10.5.0", - "@serialport/parser-ready": "10.5.0", - "@serialport/parser-regex": "10.5.0", - "@serialport/parser-slip-encoder": "10.5.0", - "@serialport/parser-spacepacket": "10.5.0", - "@serialport/stream": "10.5.0", - "debug": "^4.3.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, "node_modules/sharp": { "version": "0.33.2", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", @@ -2508,10 +2310,6 @@ "node": ">=8" } }, - "node_modules/slip": { - "version": "1.0.2", - "license": "(MIT OR GPL-2.0)" - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -2721,10 +2519,6 @@ "node": ">= 8" } }, - "node_modules/wolfy87-eventemitter": { - "version": "5.2.9", - "license": "Unlicense" - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/package.json b/package.json index 55f30d7..ced90b5 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,14 @@ "fast-xml-parser": "^4.3.4", "iconv-lite": "^0.6.3", "mime-types": "^2.1.35", - "osc": "^2.4.4", + "node-osc": "^9.1.0", "sharp": "^0.33.2", "ws": "^8.16.0" }, "devDependencies": { + "@types/mime-types": "^2.1.4", "@types/node": "^20.11.16", + "@types/node-osc": "^6.0.3", "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", @@ -25,6 +27,7 @@ "typescript": "^5.3.3" }, "scripts": { + "lint": "eslint src/**/*.ts", "build-release": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./build.ps1", "build-server": "esbuild src/server/main.ts --outfile=dist/build/main.js --tsconfig=tsconfig_server.json --platform=node --minify --bundle", "build-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=tsconfig_client.json --bundle --minify", diff --git a/src/client/main.ts b/src/client/main.ts index e7e8f27..9d32e83 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -1,10 +1,11 @@ import MessageLog from "./message_box.js"; -import * as JGCPSend from "../server/JGCPSendMessages"; import { ItemPartClient } from "../server/SequenceItems/SongFile"; import { ClientItemSlides } from "../server/SequenceItems/SequenceItem"; -import { ClientSequenceItems } from "../server/Sequence"; -import { CountdownProps } from "../server/SequenceItems/Countdown"; + +import * as JGCPSend from "../server/JGCPSendMessages"; +import * as JGCPRecv from "../server/JGCPReceiveMessages.js"; +import { ActiveItemSlide } from "../server/Sequence.js"; const config = { websocket: { @@ -12,10 +13,10 @@ const config = { } }; -const msg_log = new MessageLog(document.querySelector("#error_container")!); +const msg_log = new MessageLog(document.querySelector("#error_container")); -function open_sequence(e) { - const file = e.target.files[0]; +function open_sequence(e: Event) { + const file = (e.target).files[0]; const reader = new FileReader(); reader.onload = function(e) { @@ -31,7 +32,7 @@ function open_sequence(e) { } document.querySelector("#input_open_sequence")?.addEventListener("change", open_sequence); -function button_navigate(type, steps) { +function button_navigate(type: JGCPRecv.NavigateType, steps: -1 | 1) { ws.send(JSON.stringify({ command: "navigate", type, @@ -44,7 +45,7 @@ document.querySelector("#navigate_item_next")?.addEventListener("click", () => { document.querySelector("#navigate_slide_prev")?.addEventListener("click", () => { button_navigate("slide", -1); }); document.querySelector("#navigate_slide_next")?.addEventListener("click", () => { button_navigate("slide", 1); }); -function button_visibility(visibility) { +function button_visibility(visibility: boolean) { ws.send(JSON.stringify({ command: "set_visibility", visibility, @@ -56,7 +57,7 @@ document.querySelector("#set_visibility_show")?.addEventListener("click", () => document.querySelector("#show_error_log")?.addEventListener("click", () => msg_log.error("foobar")); -function display_items(data: ClientSequenceItems) { +function display_items(data: JGCPSend.Sequence) { const div_sequence_items = document.querySelector("#sequence_items"); // initialize @@ -87,7 +88,7 @@ function display_items(data: ClientSequenceItems) { // if it's a Countdown-Object, insert the time if (item.type === "Countdown") { - div_sequence_item.innerText = item.Caption.replace("%s", (item as CountdownProps).Time); + div_sequence_item.innerText = item.Caption.replace("%s", item.Time); } else { div_sequence_item.innerText = item.Caption; } @@ -100,7 +101,7 @@ function display_items(data: ClientSequenceItems) { display_visibility_state(data.metadata.visibility); } -function request_item_slides(item) { +function request_item_slides(item: number) { if (item !== selected_item_number) { selected_item_number = item; @@ -338,7 +339,7 @@ function set_active_slide(scroll: boolean = false) { } } -function set_active_item_slide(data, message_client_id) { +function set_active_item_slide(data: ActiveItemSlide, message_client_id: string) { // store the data active_item_slide = data; @@ -359,7 +360,7 @@ function set_active_item_slide(data, message_client_id) { } } -function display_visibility_state(state) { +function display_visibility_state(state: boolean) { const button_hide = document.querySelector("#set_visibility_hide"); const button_show = document.querySelector("#set_visibility_show"); @@ -372,15 +373,16 @@ function display_visibility_state(state) { } } -function display_state_change(data) { +function display_state_change(data: JGCPSend.State) { const key_map = { active_item_slide: set_active_item_slide, visibility: display_visibility_state }; Object.entries(data).forEach(([key, value]) => { - if (Object.keys(key_map).includes(key)) { - key_map[key](value, data.clientID); + if (key in key_map) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + key_map[key as keyof typeof key_map](value, data.client_id); } }); } @@ -389,7 +391,7 @@ function random_4_hex() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); } -function blank_screen(state) { +function blank_screen(state: boolean) { const blank_div = document.querySelector("div#blank"); if (blank_div !== null) { blank_div.style.display = state ? "unset" : "none"; @@ -416,7 +418,7 @@ function init() { } const client_id = `${random_4_hex()}-${random_4_hex()}-${random_4_hex()}-${random_4_hex()}`; -let selected_item_number; +let selected_item_number: number; function ws_connect() { const url = new URL(document.URL); @@ -430,8 +432,8 @@ function ws_connect() { blank_screen(false); }); - ws.addEventListener("message", (event) => { - const data = JSON.parse(event.data); + ws.addEventListener("message", (event: MessageEvent) => { + const data: JGCPSend.Message = JSON.parse(event.data as string) as JGCPSend.Message; console.dir(data); @@ -442,7 +444,7 @@ function ws_connect() { item_slides: display_item_slides, state: display_state_change, clear: init, - response: (response) => { + response: (response: JGCPSend.Response) => { switch (Number(response.code.toString()[0])) { case 4: msg_log.error(response.message); @@ -453,20 +455,20 @@ function ws_connect() { } }; - command_parser_map[data.command](data); + command_parser_map[data.command](data as never); }); ws.addEventListener("ping", () => { }); - ws.addEventListener("error", (event) => { + ws.addEventListener("error", (event: ErrorEvent) => { msg_log.error(`Server connection encountered error '${event.message}'. Closing socket`); ws.close(); }); - ws.addEventListener("close", async () => { + ws.addEventListener("close", () => { console.log("No connection to server. Retrying in 1s"); // blank the screen because there is no connection @@ -479,7 +481,7 @@ function ws_connect() { }); } -let ws; +let ws: WebSocket; ws_connect(); let active_item_slide = { diff --git a/tsconfig_client.json b/src/client/tsconfig.json similarity index 72% rename from tsconfig_client.json rename to src/client/tsconfig.json index 223aaf6..2c04ae4 100644 --- a/tsconfig_client.json +++ b/src/client/tsconfig.json @@ -3,11 +3,8 @@ "target": "ES2022", "module": "ES2022", "moduleResolution": "bundler", - "outDir": "client", + "outDir": "../../client", "removeComments": true, "noImplicitAny": true - }, - "include": [ - "src\\client" - ] + } } \ No newline at end of file diff --git a/src/server/JGCPReceiveMessages.ts b/src/server/JGCPReceiveMessages.ts index d2707b6..77a9d02 100644 --- a/src/server/JGCPReceiveMessages.ts +++ b/src/server/JGCPReceiveMessages.ts @@ -9,7 +9,7 @@ interface Base { * sequence-file to be loaded */ export interface OpenSequence extends Base { - command: "open-sequence"; + command: "open_sequence"; sequence: string; } @@ -17,7 +17,7 @@ export interface OpenSequence extends Base { * request for the slides of a specific item */ export interface RequestItemSlides extends Base { - command: "request-item-slides"; + command: "request-item_slides"; item: number; } @@ -32,7 +32,7 @@ export interface ItemSlideSelect extends Base { */ const item_navigate_type = ["item", "slide"] as const; export type NavigateType = (typeof item_navigate_type)[number]; -// eslint-disable-next-line @typescript-eslint/no-explicit-any +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument export const is_item_navigate_type = (x: any): x is NavigateType => item_navigate_type.includes(x); export interface Navigate extends Base { @@ -45,12 +45,19 @@ export interface Navigate extends Base { * set the visibility of the output */ export interface SetVisibility extends Base { - command: "set-visibility"; + command: "set_visibility"; visibility: boolean; } +export interface SelectItemSlide extends Base { + command: "select_item_slide"; + item: number; + slide: number; + +} + /** * Uniun of the different JGCP-messages */ -export type Message = RequestItemSlides | SetVisibility | OpenSequence | Navigate; +export type Message = RequestItemSlides | SetVisibility | OpenSequence | Navigate | SelectItemSlide; diff --git a/src/server/JGCPSendMessages.ts b/src/server/JGCPSendMessages.ts index 5e9243c..03b22ad 100644 --- a/src/server/JGCPSendMessages.ts +++ b/src/server/JGCPSendMessages.ts @@ -35,12 +35,6 @@ export interface State extends Base { visibility?: boolean; } -// export interface ItemSlides extends Base, ClientItemSlidesBase { -// clientID: string; -// command: "item-slides"; -// slides: string[][]; -// } - interface ItemSlidesBase extends Base{ client_id: string; command: "item_slides"; diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index d920bab..eebf8ed 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -61,17 +61,19 @@ class Sequence { const xml_parser = new XMLParser(); // create the casparcg-connections + // eslint-disable-next-line @typescript-eslint/no-misused-promises Config.casparcg.connections.forEach(async (connection_setting) => { - const connection = new CasparCG({ + const connection: CasparCG = new CasparCG({ ...connection_setting, // eslint-disable-next-line @typescript-eslint/naming-convention autoConnect: true }); - const casparcg_connection = { + const casparcg_connection: CasparCGConnection = { connection, settings: connection_setting, - paths: xml_parser.parse((await (await connection.infoPaths()).request)?.data as string ?? "")?.paths, + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + paths: xml_parser.parse((await (await connection.infoPaths()).request)?.data as string ?? "")?.paths as CasparCGPathsSettings, media: (await (await connection.cls()).request)?.data ?? [] }; @@ -107,12 +109,12 @@ class Sequence { * clear the casparcg-layers used */ casparcg_clear_layers(casparcg_connection?: CasparCGConnection) { - const clear_layers = (connection) => { - connection.connection.cgClear({ + const clear_layers = (connection: CasparCGConnection) => { + void connection.connection.cgClear({ channel: connection.settings.channel, layer: connection.settings.layers[0] }); - connection.connection.cgClear({ + void connection.connection.cgClear({ channel: connection.settings.channel, layer: connection.settings.layers[1] }); @@ -256,6 +258,7 @@ class Sequence { // navigate_item(direction: NavigateDirection, slide: number = 0): void { navigate_item(steps: number, slide: number = 0): void { if (typeof steps !== "number") { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new TypeError(`steps ('${steps}') is no number`); } @@ -348,13 +351,14 @@ class Sequence { // if no connection was give, flip the layers if (casparcg_connection === undefined) { // clear the lower layer - connections.forEach((casparcg_connection) => { - casparcg_connection.connection.cgClear({ + // eslint-disable-next-line @typescript-eslint/no-misused-promises + connections.forEach(async (casparcg_connection) => { + await casparcg_connection.connection.cgClear({ channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[0] }); - casparcg_connection.connection.swap({ + void casparcg_connection.connection.swap({ channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[0], channel2: casparcg_connection.settings.channel, @@ -364,22 +368,24 @@ class Sequence { }); } + // eslint-disable-next-line @typescript-eslint/no-misused-promises connections.forEach(async (casparcg_connection) => { // generate the render-object const render_object = await this.active_sequence_item.create_render_object(); // depending on the type, use different caspar-cg-functions switch (render_object?.caspar_type) { - case "template": - casparcg_connection.connection.cgAdd({ + case "template": { + + void casparcg_connection.connection.cgAdd({ /* eslint-disable @typescript-eslint/naming-convention */ channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[1], cgLayer: 0, playOnLoad: this.casparcg_visibility, - template: Config.casparcg.templates[this.active_sequence_item.props.type], + template: Config.casparcg.templates[this.active_sequence_item.props.type] as string, // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug - data: JSON.stringify(JSON.stringify(render_object, (_key, val) => { + data: JSON.stringify(JSON.stringify(render_object, (_key, val: unknown) => { if (typeof val === "string") { return val.replace("\"", "\\u0022"); } else { @@ -388,7 +394,7 @@ class Sequence { })) /* eslint-enable @typescript-eslint/naming-convention */ }); - break; + } break; case "media": { // make it all uppercase and remove the extension to match casparcg-clips and make sure it uses forward slashes const req_name = render_object.file_name.replace(/\.[^(\\.]+$/, "").toUpperCase().replace(/\\/g, "/"); @@ -407,7 +413,7 @@ class Sequence { // if a matching media-file was found, use it if (media_result !== undefined) { - casparcg_connection.connection.play({ + void casparcg_connection.connection.play({ /* eslint-disable @typescript-eslint/naming-convention */ channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[1], @@ -419,7 +425,7 @@ class Sequence { /* eslint-enable @typescript-eslint/naming-convention */ }); } else { - casparcg_connection.connection.executeCommand({ + void casparcg_connection.connection.executeCommand({ /* eslint-disable @typescript-eslint/naming-convention */ command: Commands.PlayHtml, params: { @@ -442,7 +448,7 @@ class Sequence { private casparcg_select_slide(slide: number): void { this.casparcg_connections.forEach((casparcg_connection) => { // jump to the slide-number in casparcg - casparcg_connection.connection.cgInvoke({ + void casparcg_connection.connection.cgInvoke({ /* eslint-disable @typescript-eslint/naming-convention */ channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[1], @@ -453,10 +459,10 @@ class Sequence { }); } - set_visibility(visibility: boolean): void { - this.casparcg_connections.forEach((casparcg_connection) => { + async set_visibility(visibility: boolean): Promise { + for (const casparcg_connection of this.casparcg_connections) { // clear the background-layer, so that the foreground has an hide-animation - casparcg_connection.connection.clear({ + await casparcg_connection.connection.clear({ channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[0] }); @@ -472,11 +478,11 @@ class Sequence { this.casparcg_visibility = visibility; if (visibility) { - casparcg_connection.connection.cgPlay(options); + void casparcg_connection.connection.cgPlay(options); } else { - casparcg_connection.connection.cgStop(options); + void casparcg_connection.connection.cgStop(options); } - }); + } } get active_item(): number { @@ -516,7 +522,7 @@ function parse_item_value_string(key: string, value: string): { [P in keyof Item // remove line-breaks value = value.replace(/'\s\+\s+'/gm, ""); // un-escape escaped characters - value = value.replace(/'#(\d+)'/gm, (match, group) => String.fromCharCode(group)); + value = value.replace(/'#(\d+)'/gm, (match, group: string) => String.fromCharCode(Number(group))); const return_props: { [P in keyof ItemProps]?: ItemProps[P]; } = {}; @@ -534,8 +540,12 @@ function parse_item_value_string(key: string, value: string): { [P in keyof Item // assume the type from the file-extension if (path.extname(value) === ".sng") { return_props.type = "Song"; - } else if (mime.lookup(value).split("/", 1)[0] === "image") { - return_props.type = "Image"; + } else { + const mime_type = mime.lookup(value); + + if (mime_type ? mime_type.split("/", 1)[0] === "image" : false) { + return_props.type = "Image"; + } } return_props[key] = value; break; @@ -718,7 +728,13 @@ function convert_color_to_hex(color: string): string | undefined { clyellowgreen: "#9acd32" }; - return colours[color.toLowerCase()]; + const return_value: unknown = colours[color.toLowerCase()]; + + if (typeof return_value !== "string") { + return ""; + } else { + return return_value; + } } // export { NavigateType, isItemNavigateType, ClientSequenceItems, ClientItemSlides, ActiveItemSlide }; diff --git a/src/server/SequenceItems/Comment.ts b/src/server/SequenceItems/Comment.ts index acaa04d..219e3af 100644 --- a/src/server/SequenceItems/Comment.ts +++ b/src/server/SequenceItems/Comment.ts @@ -27,23 +27,25 @@ export default class Comment extends SequenceItemBase { this.item_props.selectable = false; } - async create_client_object_item_slides(): Promise { - return { - type: "Comment", - title: this.props.Caption, - item: this.props.item, - slides: [], - slides_template: { - caspar_type: "media", + create_client_object_item_slides(): Promise { + return new Promise((resolve) => { + resolve({ + type: "Comment", + title: this.props.Caption, + item: this.props.item, slides: [], - slide: 0, - mute_transition: true - } - }; + slides_template: { + caspar_type: "media", + slides: [], + slide: 0, + mute_transition: true + } + }); + }); } - async create_render_object(): Promise { - return undefined; + create_render_object(): Promise { + return new Promise((resolve) => resolve(undefined)); } navigate_slide(steps: number): number { @@ -55,8 +57,8 @@ export default class Comment extends SequenceItemBase { return 0; } - protected async get_background_image(): Promise { - return ""; + protected get_background_image(): Promise { + return new Promise((resolve) => resolve("")); } get active_slide(): number { diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts index d5ee286..a5d5718 100644 --- a/src/server/SequenceItems/Countdown.ts +++ b/src/server/SequenceItems/Countdown.ts @@ -92,6 +92,7 @@ export default class Countdown extends SequenceItemBase { navigate_slide(steps: number): number { if (typeof steps !== "number") { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new TypeError(`steps ('${steps}') is no number`); } @@ -154,7 +155,7 @@ export default class Countdown extends SequenceItemBase { await this.load_background_images(image_path, this.props.background_color); } - return this.props.BackgroundImage![proxy ? "proxy" : "orig"]; + return this.props.BackgroundImage[proxy ? "proxy" : "orig"]; } get active_slide(): number { @@ -167,7 +168,7 @@ export default class Countdown extends SequenceItemBase { } } -function parse_hex_data(data_hex): CountdownData { +function parse_hex_data(data_hex: string): CountdownData { const regex_curse = /(?:546578745374796C6573060[1-3](?42)?(?49)?(?55)?|54657874436F6C6F72(?:(?:04|0707)(?[0-9A-F]{6}|(?:[0-9A-F]{2})+?)0)|466F6E744E616D650604(?(?:[A-F0-9]{2})+?)09|547970020(?[0-3])09|506F736974696F6E5802(?[A-F0-9]{2})09|506F736974696F6E5902(?[A-F0-9]{2})08|466F6E7453697A6502(?[A-F0-9]{2})0F|4261636B67726F756E64496D616765(?:[A-F0-9]{4}636F6C6F723A2F2F244646(?[A-F0-9]{12})|(?:[A-F0-9]{2})*?0[0-F]{3}(?(?:[0-9A-F]{2})+?))0|53686F775365636F6E647308(?.))/g; @@ -183,11 +184,11 @@ function parse_hex_data(data_hex): CountdownData { show_seconds: true }; - const to_string = (raw) => Buffer.from(raw, "hex").toString(); + const to_string = (raw: string): string => Buffer.from(raw, "hex").toString(); - const to_int = (r) => parseInt(r, 16); + const to_int = (r: string): number => parseInt(r, 16); - const to_rgb = (r) => { + const to_rgb = (r: string): string => { // if it is longer than 6 bytes, it is an colorName if (r.length > 6) { return convert_color_to_hex(to_string(r)); @@ -206,10 +207,10 @@ function parse_hex_data(data_hex): CountdownData { data.mode = countdown_mode_items[Number(val)]; break; case "color": - data.font_format!.color = to_rgb(val); + data.font_format.color = to_rgb(val); break; case "font_size": - data.font_format!.fontSize = to_int(val); + data.font_format.fontSize = to_int(val); break; case "x": case "y": @@ -219,16 +220,16 @@ function parse_hex_data(data_hex): CountdownData { data.show_seconds = !val; break; case "bold": - data.font_format!.fontWeight = "bold"; + data.font_format.fontWeight = "bold"; break; case "italic": - data.font_format!.fontStyle = "italic"; + data.font_format.fontStyle = "italic"; break; case "underline": - data.font_format!.textDecoration = "underline"; + data.font_format.textDecoration = "underline"; break; case "font_family": - data.font_format!.fontFamily = to_string(val); + data.font_format.fontFamily = to_string(val); break; case "background_image": data.background_image = to_string(val); diff --git a/src/server/SequenceItems/Image.ts b/src/server/SequenceItems/Image.ts index 9522486..83d7bd2 100644 --- a/src/server/SequenceItems/Image.ts +++ b/src/server/SequenceItems/Image.ts @@ -65,6 +65,7 @@ export default class Image extends SequenceItemBase { navigate_slide(steps: number): number { if (typeof steps !== "number") { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new TypeError(`steps ('${steps}') is no number`); } @@ -87,7 +88,7 @@ export default class Image extends SequenceItemBase { await this.load_background_images(filename); } - return this.props.BackgroundImage![proxy ? "proxy" : "orig"]; + return this.props.BackgroundImage[proxy ? "proxy" : "orig"]; } get active_slide(): number { diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index 114f44d..5cf1670 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -86,7 +86,7 @@ export abstract class SequenceItemBase { const slide_count = this.slide_count; if (typeof slide !== "number") { - throw new TypeError(`'${slide} is not of type 'number'`); + throw new TypeError(`'${JSON.stringify(slide)} is not of type 'number'`); } if (slide < -slide_count || slide >= slide_count) { diff --git a/src/server/SequenceItems/Song.ts b/src/server/SequenceItems/Song.ts index 4ac5750..955e6c3 100644 --- a/src/server/SequenceItems/Song.ts +++ b/src/server/SequenceItems/Song.ts @@ -56,9 +56,9 @@ export default class Song extends SequenceItemBase { try { this.song_file = new SongFile(get_song_path(props.FileName)); - } catch (e) { + } catch (e: unknown) { // if the error is because the file doesn't exist, skip the rest of the loop iteration - if (e.code === "ENOENT") { + if (e instanceof Error && "type" in e && e.type === "ENOENT") { console.debug(`song '${props.FileName}' does not exist`); return; } else { @@ -164,6 +164,7 @@ export default class Song extends SequenceItemBase { navigate_slide(steps: number): number { if (typeof steps !== "number") { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new TypeError(`steps ('${steps}') is no number`); } @@ -230,7 +231,7 @@ export default class Song extends SequenceItemBase { await this.load_background_images(image_path); } - return this.props.BackgroundImage![proxy ? "proxy" : "orig"]; + return this.props.BackgroundImage[proxy ? "proxy" : "orig"]; } get active_slide(): number { diff --git a/src/server/SequenceItems/SongFile.ts b/src/server/SequenceItems/SongFile.ts index 65daa12..ce9cf12 100644 --- a/src/server/SequenceItems/SongFile.ts +++ b/src/server/SequenceItems/SongFile.ts @@ -162,7 +162,7 @@ class SongFile { // utf-8-BOM const bom = Buffer.from([239, 187, 191]); - let encoding; + let encoding: string; // check wether the song-file starts with the utf-8-BOM if (raw_data_buffer.subarray(0, 3).compare(bom) === 0) { @@ -212,7 +212,7 @@ class SongFile { lines.push(""); } - const slide: string[][] = Array.from(Array(Math.ceil(lines.length / this.metadata.LangCount)), () => []); + const slide: string[][] = Array.from(Array(Math.ceil(lines.length / this.metadata.LangCount)), (): string[] => []); // split the lines into the different languages lines.forEach((vv, ii) => { diff --git a/src/server/config.ts b/src/server/config.ts index 8aa04c3..a30bb89 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -47,6 +47,6 @@ const config_path = "config.json"; // eslint-disable-next-line @typescript-eslint/naming-convention -const Config: ConfigJSON = JSON.parse(fs.readFileSync(config_path, { encoding: "utf-8" })); +const Config: ConfigJSON = JSON.parse(fs.readFileSync(config_path, { encoding: "utf-8" })) as ConfigJSON; export default Config; diff --git a/src/server/control.ts b/src/server/control.ts index f1fb174..224d4f5 100644 --- a/src/server/control.ts +++ b/src/server/control.ts @@ -26,7 +26,7 @@ class Control { } }, output: { - visibility: (value: boolean) => this.set_visibility(value) + visibility: async (value: boolean) => this.set_visibility(value) } } }; @@ -91,7 +91,7 @@ class Control { if (typeof item === "number") { const message: JGCPSend.ItemSlides = { command: "item_slides", - client_id: client_id!, + client_id, ...await this.sequence.create_client_object_item_slides(item) }; @@ -99,7 +99,8 @@ class Control { ws_send_response("slides have been sent", true, ws); } else { - ws_send_response(`'${item} is not of type 'number'`, false, ws); + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + ws_send_response(`'${item}' is not of type 'number'`, false, ws); } } @@ -181,7 +182,7 @@ class Control { * set the visibility of the sequence in the renderer * @param visibility wether the output should be visible (true) or not (false) */ - private set_visibility(visibility: boolean, client_id?: string, ws?: WebSocket) { + private async set_visibility(visibility: boolean, client_id?: string, ws?: WebSocket): Promise { // if there is no sequence loaded, send a negative response back and exit if (this.sequence === undefined) { ws_send_response("no schedule loaded", false, ws); @@ -189,7 +190,7 @@ class Control { } if (typeof visibility === "boolean") { - this.sequence.set_visibility(visibility); + await this.sequence.set_visibility(visibility); this.send_all_clients({ command: "state", @@ -216,7 +217,7 @@ class Control { // dummy-implementation-example if (message.command === "state" && message.visibility !== undefined) { - this.osc_server.send_value("/not/implemented", message.visibility); + this.osc_server.send_value("/custom-variable/johncg_visibility/value", message.visibility); } } @@ -247,9 +248,10 @@ class Control { } private ws_on_message(ws: WebSocket, raw_data: RawData) { - let data; + let data: JGCPRecv.Message; // try to parse the data as a JSON-object try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-base-to-string data = JSON.parse(raw_data.toString()); // if the parsed is null or not an object, throw an exception @@ -273,6 +275,7 @@ class Control { } else if (!Object.keys(this.ws_function_map).includes(data.command)) { ws_send_response(`command '${data.command}' is not implemented`, false, ws); } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call this.ws_function_map[data.command](data, ws); } } diff --git a/src/server/servers/http-server.ts b/src/server/servers/http-server.ts index 176eda9..4dbce6b 100644 --- a/src/server/servers/http-server.ts +++ b/src/server/servers/http-server.ts @@ -20,7 +20,7 @@ class HTTPServer { let resource_dir = "client"; // unescape the percent signs in the url - request.url = unescape(request.url!); + request.url = unescape(request.url); // override different requested urls switch (true) { @@ -51,9 +51,11 @@ class HTTPServer { response.write("Resource not found"); } else { + const mime_type = mime.lookup(request.url); + response.writeHead(200, { // eslint-disable-next-line @typescript-eslint/naming-convention - "Content-Type": mime.lookup(request.url) + "Content-Type": mime_type ? mime_type : "text/plain" }); // serve the file-content diff --git a/src/server/servers/osc-server.ts b/src/server/servers/osc-server.ts index f1ffaf1..0a31046 100644 --- a/src/server/servers/osc-server.ts +++ b/src/server/servers/osc-server.ts @@ -1,4 +1,4 @@ -import osc from "osc"; +import { Server as ServerOSC, Client as ClientOSC, ArgumentType as ArgumentTypeOSC } from "node-osc"; interface OSCServerArguments { port_receive: number; @@ -6,54 +6,53 @@ interface OSCServerArguments { port_send: number; } -type OSCValueType = boolean | number | string; +type OSCFunctionMap = { [key: string]: OSCFunctionMap | ((value: boolean) => void | Promise ) | ((value: number) => void | Promise ) | ((value: string) => void | Promise ) }; -type OSCFunctionMap = { [key: string]: OSCFunctionMap | ((value: boolean) => void ) | ((value: number) => void ) | ((value: string) => void ) }; +type FunctionMap = { [key: string]: FunctionMap | ((value: ArgumentTypeOSC) => void)}; class OSCServer { - private osc_server: osc.UDPPort; + // private osc_server: osc.UDPPort; + private osc_server: ServerOSC; + private osc_client: ClientOSC; - private function_map; + private function_map: FunctionMap; - constructor(args: OSCServerArguments, function_map) { + constructor(args: OSCServerArguments, function_map: OSCFunctionMap) { this.function_map = function_map; - this.osc_server = new osc.UDPPort({ - /* eslint-disable @typescript-eslint/naming-convention */ - localAddress: "0.0.0.0", - localPort: args.port_receive, - remoteAddresse: args.address_send, - remotePort: args.port_send - /* eslint-enable @typescript-eslint/naming-convention */ - }); + this.osc_server = new ServerOSC(args.port_receive, "0.0.0.0"); + + this.osc_client = new ClientOSC(args.address_send, args.port_send); this.osc_server.on("message", (osc_msg) => { - const parts = osc_msg.address.split("/"); + const parts = osc_msg[0].split("/"); // remove the first empty elementn from the leading slash parts.shift(); // execute the command map - this.execute_command(parts, this.function_map, osc_msg.args[0]); + this.execute_command(parts, this.function_map, osc_msg[1]); }); - - this.osc_server.open(); } - private execute_command(path: string[], command_tree, value) { + private execute_command(path: string[], command_tree: FunctionMap, value: ArgumentTypeOSC) { if (path.length > 1) { - const traversed_command_tree = command_tree[path.shift()!]; + const traversed_command_tree = command_tree[path.shift()]; - if (traversed_command_tree !== undefined) { + if (typeof traversed_command_tree === "object") { this.execute_command(path, traversed_command_tree, value); } } else { - command_tree[path[0]]?.(value); + const command = command_tree[path[0]]; + + if (typeof command === "function") { + command(value); + } } } - send_value(path: string, value: OSCValueType) { - let type: OSCValueType; + send_value(path: string, value: ArgumentTypeOSC) { + let type: string; switch (typeof value) { case "number": @@ -73,15 +72,7 @@ class OSCServer { return; } - this.osc_server.send({ - address: path, - args: [ - { - type, - value - } - ] - }); + this.osc_client.send([path, value]); } } diff --git a/src/server/servers/websocket-server.ts b/src/server/servers/websocket-server.ts index 533ef2f..bb66441 100644 --- a/src/server/servers/websocket-server.ts +++ b/src/server/servers/websocket-server.ts @@ -40,7 +40,7 @@ class WebsocketServer { this.message_handlers = message_handlers; this.ws_server = new WebSocketServer({ port: args.port }); - this.ws_server.on("connection", (ws, socket, request) => this.on_connection(ws, socket, request)); + this.ws_server.on("connection", (ws: WebSocket, socket: WebSocket, request: IncomingMessage) => this.on_connection(ws, socket, request)); } private on_connection(ws: WebSocket,socket: WebSocket, request: IncomingMessage) { @@ -53,13 +53,15 @@ class WebsocketServer { this.connections[ws.protocol].push(ws); // register the different action-handlers - Object.keys(this.message_handlers[ws.protocol]).forEach((s_type) => { - ws.on(s_type, (...data) => this.message_handlers[ws.protocol][s_type](ws, ...data)); + Object.keys(this.message_handlers[ws.protocol]).forEach((s_type: keyof MessageHandler) => { + ws.on(s_type, (...args: [never, never]) => { + this.message_handlers[ws.protocol][s_type](ws, ...args); + }); }); // execute the on_connection function if (this.message_handlers[ws.protocol].connection !== undefined) { - this.message_handlers[ws.protocol].connection!(ws, socket, request); + this.message_handlers[ws.protocol].connection(ws, socket, request); } } else { // reject connection diff --git a/tsconfig_server.json b/src/server/tsconfig.json similarity index 53% rename from tsconfig_server.json rename to src/server/tsconfig.json index c43ef71..aa49ee0 100644 --- a/tsconfig_server.json +++ b/src/server/tsconfig.json @@ -2,11 +2,9 @@ "compilerOptions": { "target": "ES2022", "module": "NodeNext", - "outDir": "out", + "outDir": "../../out", "sourceMap": true, - "removeComments": true - }, - "include": [ - "src\\server" - ] + "removeComments": true, + // "noImplicitAny": true + } } \ No newline at end of file diff --git a/src/templates/Countdown.ts b/src/templates/Countdown.ts index 8ebf66d..3e7bb13 100644 --- a/src/templates/Countdown.ts +++ b/src/templates/Countdown.ts @@ -1,6 +1,6 @@ import { CountdownRenderObject } from "../server/SequenceItems/Countdown"; -let update_interval; +let update_interval: NodeJS.Timeout; const spans: { hours?: HTMLSpanElement[], minutes: HTMLSpanElement[], @@ -17,11 +17,11 @@ let end_time_sign; // eslint-disable-next-line @typescript-eslint/no-unused-vars -function update(str_args) { +function update(str_args: string) { // clear the old-state clearInterval(update_interval); - data = JSON.parse(str_args); + data = JSON.parse(str_args) as CountdownRenderObject; // if requested, diable transition-effects const main_div = document.querySelector("div#main"); @@ -141,7 +141,7 @@ function stop() { function next() { } -function get_remaining_time(): [{ hours: string[], minutes: string[], seconds: string[] }, boolean] { +function get_remaining_time(): [Record, boolean] { const time_remaining = new Date(end_time.getTime() - Date.now()); return [ @@ -185,7 +185,10 @@ function position_time() { } function update_time() { - const [time, finished] = get_remaining_time(); + const [time, finished]: [ + Record, + boolean + ] = get_remaining_time(); if (finished) { clearInterval(update_interval); @@ -205,8 +208,8 @@ function update_time() { } Object.entries(spans).forEach(([key, val]) => { - val[0].innerText = time[key][0]; - val[1].innerText = time[key][1]; + val[0].innerText = time[key as keyof typeof spans][0]; + val[1].innerText = time[key as keyof typeof spans][1]; }); } } \ No newline at end of file diff --git a/src/templates/Song.ts b/src/templates/Song.ts index 111fa7a..651b367 100644 --- a/src/templates/Song.ts +++ b/src/templates/Song.ts @@ -9,7 +9,7 @@ let slide_count = 0; // eslint-disable-next-line @typescript-eslint/no-unused-vars function update(s_data: string) { // parse the transferred data into json - data = JSON.parse(s_data); + data = JSON.parse(s_data) as SongRenderObject; // get the div for the display and storage const div_container = document.querySelector("div#container"); diff --git a/tsconfig_templates.json b/src/templates/tsconfig.json similarity index 80% rename from tsconfig_templates.json rename to src/templates/tsconfig.json index 12cd147..3ebc71c 100644 --- a/tsconfig_templates.json +++ b/src/templates/tsconfig.json @@ -5,8 +5,5 @@ "moduleResolution": "bundler", "outDir": "casparcg-templates/JohnCG", "removeComments": true - }, - "include": [ - "src\\templates" - ] + } } \ No newline at end of file From 6bd1019d5fbfc1efaddaa9e2b816905c26d5a9c3 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Fri, 23 Feb 2024 21:19:36 +0100 Subject: [PATCH 11/16] Bugfix: media now transitions again without blackout --- README.md | 4 +-- src/server/Sequence.ts | 56 ++++++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 708a694..6aa392d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,4 @@ Generate lyric-graphics and play them out through CasparCG. - add option to change template-directory - make sequence-comments another design in client - client-messages: create message-log, group same -- repo-file-managment (move tsconfig files in respective directories?) -- build-script in node / integrate with license-generator -- media-to-media-switch: there is no switching to background-layer \ No newline at end of file +- build-script in node / integrate with license-generator \ No newline at end of file diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index eebf8ed..9ebcaff 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -348,28 +348,26 @@ class Sequence { private casparcg_load_item(casparcg_connection?: CasparCGConnection): void { const connections = casparcg_connection ? [casparcg_connection] : this.casparcg_connections; - // if no connection was give, flip the layers - if (casparcg_connection === undefined) { - // clear the lower layer - // eslint-disable-next-line @typescript-eslint/no-misused-promises - connections.forEach(async (casparcg_connection) => { - await casparcg_connection.connection.cgClear({ - channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[0] + // eslint-disable-next-line @typescript-eslint/no-misused-promises + connections.forEach(async (connection) => { + // if no connection was give, flip the layers + if (casparcg_connection === undefined) { + // clear the lower layer + // eslint-disable-next-line @typescript-eslint/no-misused-promises + await connection.connection.cgClear({ + channel: connection.settings.channel, + layer: connection.settings.layers[0] }); - - void casparcg_connection.connection.swap({ - channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[0], - channel2: casparcg_connection.settings.channel, - layer2: casparcg_connection.settings.layers[1], + + await connection.connection.swap({ + channel: connection.settings.channel, + layer: connection.settings.layers[0], + channel2: connection.settings.channel, + layer2: connection.settings.layers[1], transforms: true }); - }); - } - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - connections.forEach(async (casparcg_connection) => { + } + // generate the render-object const render_object = await this.active_sequence_item.create_render_object(); @@ -377,10 +375,10 @@ class Sequence { switch (render_object?.caspar_type) { case "template": { - void casparcg_connection.connection.cgAdd({ + void connection.connection.cgAdd({ /* eslint-disable @typescript-eslint/naming-convention */ - channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[1], + channel: connection.settings.channel, + layer: connection.settings.layers[1], cgLayer: 0, playOnLoad: this.casparcg_visibility, template: Config.casparcg.templates[this.active_sequence_item.props.type] as string, @@ -402,7 +400,7 @@ class Sequence { let media_result: ClipInfo | undefined; // check all the casparcg-files, wether they contain a media-file that matches the path - for (const m of casparcg_connection.media) { + for (const m of connection.media) { const media_file = m.clip.toUpperCase().replace(/\\/, "/"); if (req_name.endsWith(media_file)) { @@ -413,10 +411,10 @@ class Sequence { // if a matching media-file was found, use it if (media_result !== undefined) { - void casparcg_connection.connection.play({ + void connection.connection.play({ /* eslint-disable @typescript-eslint/naming-convention */ - channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[1], + channel: connection.settings.channel, + layer: connection.settings.layers[1], clip: media_result.clip, transition: { duration: 12.5, // 25 frames = 1s@25fps @@ -425,12 +423,12 @@ class Sequence { /* eslint-enable @typescript-eslint/naming-convention */ }); } else { - void casparcg_connection.connection.executeCommand({ + void connection.connection.executeCommand({ /* eslint-disable @typescript-eslint/naming-convention */ command: Commands.PlayHtml, params: { - channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[1], + channel: connection.settings.channel, + layer: connection.settings.layers[1], url: JSON.stringify(render_object.background_image), transition: { duration: 12.5, // 25 frames = 1s@25fps From 8cc736c37a2aec1c83d54b5213a12abc61c35e46 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Sat, 24 Feb 2024 04:30:18 +0100 Subject: [PATCH 12/16] Added command-comments to implement functionalty beyond songbeamer without an own editor --- .vscode/tasks.json | 14 +++ README.md | 4 +- config.json | 13 +- package.json | 10 +- src/client/main.ts | 39 +++++- src/server/JGCPSendMessages.ts | 3 + src/server/Sequence.ts | 133 ++++++++++++++------- src/server/SequenceItems/CommandComment.ts | 83 +++++++++++++ src/server/SequenceItems/Image.ts | 3 +- src/server/SequenceItems/SequenceItem.ts | 9 +- src/server/config.ts | 12 +- src/server/servers/http-server.ts | 2 +- 12 files changed, 253 insertions(+), 72 deletions(-) create mode 100644 src/server/SequenceItems/CommandComment.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ec87393..63e6576 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,6 +8,20 @@ "problemMatcher": [], "label": "build release" }, + { + "type": "npm", + "script": "watch-client", + "group": "build", + "problemMatcher": [], + "label": "watch-client" + }, + { + "type": "npm", + "script": "watch-templates", + "group": "build", + "problemMatcher": [], + "label": "watch-templates" + }, { "type": "typescript", "tsconfig": "src/server/tsconfig.json", diff --git a/README.md b/README.md index 6aa392d..58cdd2a 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,6 @@ Generate lyric-graphics and play them out through CasparCG. - add option to change template-directory - make sequence-comments another design in client - client-messages: create message-log, group same -- build-script in node / integrate with license-generator \ No newline at end of file +- build-script in node / integrate with license-generator +- command-comment: define commands / names which get loaded straigth from the start and can be shown anytime +- song: transmit template in server->client message instead of hardcoding it into the client \ No newline at end of file diff --git a/config.json b/config.json index 03227a3..50fd0c6 100644 --- a/config.json +++ b/config.json @@ -7,19 +7,16 @@ "song": "D:/path/to/song/directory" }, "casparcg": { - "templates": { - "Song": "JohnCG/Song", - "Countdown": "JohnCG/Countdown" - }, + "templates": "E:/path/to/casparcg/templates", "connections": [ { "host": "127.0.0.1", "port": 5250, "channel": 1, - "layers": [ - 20, - 21 - ] + "layers": { + "song": [20, 21], + "command_comment": 30 + } } ] }, diff --git a/package.json b/package.json index ced90b5..8a67de8 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,10 @@ "scripts": { "lint": "eslint src/**/*.ts", "build-release": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./build.ps1", - "build-server": "esbuild src/server/main.ts --outfile=dist/build/main.js --tsconfig=tsconfig_server.json --platform=node --minify --bundle", - "build-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=tsconfig_client.json --bundle --minify", - "watch-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=tsconfig_client.json --bundle --sourcemap --watch", - "build-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=tsconfig_templates.json --target=chrome71 --minify", - "watch-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=tsconfig_templates.json --target=chrome71 --sourcemap --watch" + "build-server": "esbuild src/server/main.ts --outfile=dist/build/main.js --tsconfig=src/server/tsconfig.json --platform=node --minify --bundle", + "build-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=src/client/tsconfig.json --bundle --minify", + "watch-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=src/client/tsconfig.json --bundle --sourcemap --watch", + "build-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=src/templates/tsconfig.json --target=chrome71 --minify", + "watch-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=src/templates/tsconfig.json --target=chrome71 --sourcemap --watch" } } diff --git a/src/client/main.ts b/src/client/main.ts index 9d32e83..2dd5c07 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -141,6 +141,9 @@ function display_item_slides(data: JGCPSend.ItemSlides) { case "Image": part_arrays = create_image_countdown_slides(data as JGCPSend.ImageSlides); break; + case "CommandComment": + part_arrays = create_template_slides(data as JGCPSend.CommandCommentSlides); + break; default: console.error(`'${data.type}' is not supported`); } @@ -231,22 +234,52 @@ function create_image_countdown_slides(data: JGCPSend.CountdownSlides | JGCPSend return [div_slide_part]; } +function create_template_slides(data: JGCPSend.CommandCommentSlides): HTMLDivElement[] { + // create the container for the part + const div_slide_part = document.createElement("div"); + div_slide_part.classList.add("slide_part"); + + // create the header of the part and append it to the part-container + const div_slide_part_header = document.createElement("div"); + div_slide_part_header.classList.add("header"); + div_slide_part.append(div_slide_part_header); + + // create the slides-view and append it to the part container + const div_slides_view = document.createElement("div"); + div_slides_view.classList.add("slides_view"); + div_slide_part.append(div_slides_view); + + div_slide_part_header.innerText = data.title; + + const obj = create_slide_object(data, 0); + + div_slides_view.append(obj); + + return [div_slide_part]; +} + function create_slide_object(data: ClientItemSlides, number: number) { const div_slide_container = document.createElement("div"); div_slide_container.classList.add("slide_container"); const slide_object = document.createElement("object"); + let template_data: object = data.slides_template; + switch (data.type) { case "Song": - slide_object.data = "Templates/Song.html"; + slide_object.data = "Templates/JohnCG/Song.html"; break; case "Countdown": - slide_object.data = "Templates/Countdown.html"; + slide_object.data = "Templates/JohnCG/Countdown.html"; break; case "Image": slide_object.data = `${data.slides_template.background_image?.replace(/\\/g, "\\\\")}`; break; + case "CommandComment": + slide_object.data = `Templates/${data.slides_template.slides[0].template}.html`; + template_data = data.slides_template.slides[0].data; + break; } slide_object.classList.add("slide"); @@ -256,7 +289,7 @@ function create_slide_object(data: ClientItemSlides, number: number) { slide_object.dataset.slide_number = number.toString(); slide_object.addEventListener("load", () => { - slide_object.contentWindow?.update(JSON.stringify(data.slides_template)); + slide_object.contentWindow?.update(JSON.stringify(template_data)); switch (data.type) { case "Song": diff --git a/src/server/JGCPSendMessages.ts b/src/server/JGCPSendMessages.ts index 03b22ad..8489cf2 100644 --- a/src/server/JGCPSendMessages.ts +++ b/src/server/JGCPSendMessages.ts @@ -1,6 +1,7 @@ import * as SequenceClass from "../server/Sequence"; import { ClientCountdownSlides } from "../server/SequenceItems/Countdown"; import { ClientSongSlides } from "../server/SequenceItems/Song"; +import { ClientCommandCommentSlides } from "./SequenceItems/CommandComment"; import { ClientImageSlides } from "./SequenceItems/Image"; /** @@ -46,6 +47,8 @@ export type CountdownSlides = ItemSlidesBase & ClientCountdownSlides; export type ImageSlides = ItemSlidesBase & ClientImageSlides; +export type CommandCommentSlides = ItemSlidesBase & ClientCommandCommentSlides; + // temporary until full feature set export type NotImplementedSlides = ItemSlidesBase & { type: string; item: number; }; diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index 9ebcaff..7f72ab9 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -1,5 +1,5 @@ import path from "path"; -import { CasparCG, ClipInfo, Commands } from "casparcg-connection"; +import { CasparCG, ClipInfo, Commands, TransitionParameters } from "casparcg-connection"; import mime from "mime-types"; import { XMLParser } from "fast-xml-parser"; @@ -14,6 +14,7 @@ import Countdown, { CountdownProps } from "./SequenceItems/Countdown"; import Comment, { CommentProps } from "./SequenceItems/Comment"; import Image, { ImageProps } from "./SequenceItems/Image"; import { TransitionType } from "casparcg-connection/dist/enums"; +import CommandComment, { CommandCommentProps } from "./SequenceItems/CommandComment"; interface ClientSequenceItems { sequence_items: ItemProps[]; @@ -112,11 +113,15 @@ class Sequence { const clear_layers = (connection: CasparCGConnection) => { void connection.connection.cgClear({ channel: connection.settings.channel, - layer: connection.settings.layers[0] + layer: connection.settings.layers.song[0] }); void connection.connection.cgClear({ channel: connection.settings.channel, - layer: connection.settings.layers[1] + layer: connection.settings.layers.song[1] + }); + void connection.connection.cgClear({ + channel: connection.settings.channel, + layer: connection.settings.layers.command_comment }); }; @@ -208,7 +213,22 @@ class Sequence { default: // if it wasn't caught by other cases, it is either a comment or not implemented yet -> if there is no file specified, treat it as comment if (!Object.keys(item_data).includes("FileName")) { - this.sequence_items.push(new Comment(item_data as CommentProps)); + // try to parse the caption as an JSON-object for AMCP-Commands + const caption_json = JSON.parse(item_data.Caption) as unknown; + + if (typeof caption_json === "object") { + console.debug(caption_json); + + const props: CommandCommentProps = { + ...item_data, + type: "CommandComment", + ...caption_json as { template: string, data?: object } + }; + + this.sequence_items.push(new CommandComment(props)); + } else { + this.sequence_items.push(new Comment(item_data as CommentProps)); + } } break; } @@ -350,22 +370,30 @@ class Sequence { // eslint-disable-next-line @typescript-eslint/no-misused-promises connections.forEach(async (connection) => { - // if no connection was give, flip the layers + // if no connection was give, flip the layers and stop the template-layer if (casparcg_connection === undefined) { // clear the lower layer // eslint-disable-next-line @typescript-eslint/no-misused-promises await connection.connection.cgClear({ channel: connection.settings.channel, - layer: connection.settings.layers[0] + layer: connection.settings.layers.song[0] }); await connection.connection.swap({ channel: connection.settings.channel, - layer: connection.settings.layers[0], + layer: connection.settings.layers.song[0], channel2: connection.settings.channel, - layer2: connection.settings.layers[1], + layer2: connection.settings.layers.song[1], transforms: true }); + + void connection.connection.cgStop({ + /* eslint-disable @typescript-eslint/naming-convention */ + channel: connection.settings.channel, + layer: connection.settings.layers.command_comment, + cgLayer: 0 + /* eslint-enable @typescript-eslint/naming-convention */ + }); } // generate the render-object @@ -373,26 +401,48 @@ class Sequence { // depending on the type, use different caspar-cg-functions switch (render_object?.caspar_type) { - case "template": { - - void connection.connection.cgAdd({ - /* eslint-disable @typescript-eslint/naming-convention */ - channel: connection.settings.channel, - layer: connection.settings.layers[1], - cgLayer: 0, - playOnLoad: this.casparcg_visibility, - template: Config.casparcg.templates[this.active_sequence_item.props.type] as string, - // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug - data: JSON.stringify(JSON.stringify(render_object, (_key, val: unknown) => { - if (typeof val === "string") { - return val.replace("\"", "\\u0022"); - } else { - return val; - } - })) - /* eslint-enable @typescript-eslint/naming-convention */ - }); - } break; + case "template": + switch (this.active_sequence_item.props.type) { + case "CommandComment": + void connection.connection.cgAdd({ + /* eslint-disable @typescript-eslint/naming-convention */ + channel: connection.settings.channel, + layer: connection.settings.layers.command_comment, + cgLayer: 0, + playOnLoad: false, + template: this.active_sequence_item.props.template, + // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug + data: JSON.stringify(JSON.stringify(this.active_sequence_item.props.data, (_key, val: unknown) => { + if (typeof val === "string") { + return val.replace("\"", "\\u0022"); + } else { + return val; + } + })) + /* eslint-enable @typescript-eslint/naming-convention */ + }); + break; + default: + void connection.connection.cgAdd({ + /* eslint-disable @typescript-eslint/naming-convention */ + channel: connection.settings.channel, + layer: connection.settings.layers.song[1], + cgLayer: 0, + playOnLoad: this.casparcg_visibility, + template: `JohnCG/${render_object.type}`, + // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug + data: JSON.stringify(JSON.stringify(render_object, (_key, val: unknown) => { + if (typeof val === "string") { + return val.replace("\"", "\\u0022"); + } else { + return val; + } + })) + /* eslint-enable @typescript-eslint/naming-convention */ + }); + break; + } + break; case "media": { // make it all uppercase and remove the extension to match casparcg-clips and make sure it uses forward slashes const req_name = render_object.file_name.replace(/\.[^(\\.]+$/, "").toUpperCase().replace(/\\/g, "/"); @@ -409,17 +459,21 @@ class Sequence { } } + const transition: TransitionParameters = { + /* eslint-disable @typescript-eslint/naming-convention */ + duration: 12.5, // 25 frames = 1s@25fps + transitionType: TransitionType.Mix + /* eslint-enable @typescript-eslint/naming-convention */ + }; + // if a matching media-file was found, use it if (media_result !== undefined) { void connection.connection.play({ /* eslint-disable @typescript-eslint/naming-convention */ channel: connection.settings.channel, - layer: connection.settings.layers[1], + layer: connection.settings.layers.song[1], clip: media_result.clip, - transition: { - duration: 12.5, // 25 frames = 1s@25fps - transitionType: TransitionType.Mix - } + transition: !render_object.mute_transition ? transition : undefined /* eslint-enable @typescript-eslint/naming-convention */ }); } else { @@ -428,12 +482,9 @@ class Sequence { command: Commands.PlayHtml, params: { channel: connection.settings.channel, - layer: connection.settings.layers[1], + layer: connection.settings.layers.song[1], url: JSON.stringify(render_object.background_image), - transition: { - duration: 12.5, // 25 frames = 1s@25fps - transitionType: TransitionType.Mix - } + transition: !render_object.mute_transition ? transition : undefined /* eslint-enable @typescript-eslint/naming-convention */ } }); @@ -449,7 +500,7 @@ class Sequence { void casparcg_connection.connection.cgInvoke({ /* eslint-disable @typescript-eslint/naming-convention */ channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[1], + layer: casparcg_connection.settings.layers.song[1], cgLayer: 0, method: `jump(${slide})` /* eslint-enable @typescript-eslint/naming-convention */ @@ -462,13 +513,13 @@ class Sequence { // clear the background-layer, so that the foreground has an hide-animation await casparcg_connection.connection.clear({ channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[0] + layer: casparcg_connection.settings.layers.song[0] }); const options = { /* eslint-disable @typescript-eslint/naming-convention */ channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[1], + layer: casparcg_connection.settings.layers.song[1], cgLayer: 0 /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/server/SequenceItems/CommandComment.ts b/src/server/SequenceItems/CommandComment.ts new file mode 100644 index 0000000..1f0c67b --- /dev/null +++ b/src/server/SequenceItems/CommandComment.ts @@ -0,0 +1,83 @@ +import { ClientItemSlides, ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; + +export interface CommandCommentProps extends ItemPropsBase { + type: "CommandComment"; + template: string; + data?: object; +} + +export interface ClientCommandCommentSlides extends ClientItemSlidesBase { + type: "CommandComment"; + slides_template: CommandCommentRenderObject & { mute_transition: true }; +} + +export interface CommandCommentRenderObject extends ItemRenderObjectBase { + type: "CommandComment"; + caspar_type: "template"; + slides: [ + { + template: string; + data?: object; + } + ]; +} + +export default class CommandComment extends SequenceItemBase { + protected item_props: CommandCommentProps; + + protected slide_count: number = 0; + + constructor(props: CommandCommentProps) { + super(); + + this.item_props = props; + } + + async create_client_object_item_slides(): Promise { + return { + type: "CommandComment", + title: this.props.Caption, + item: this.props.item, + slides: [], + slides_template: { + ...await this.create_render_object(), + mute_transition: true + } + }; + } + + create_render_object(): Promise { + return new Promise((resolve) => { + resolve({ + type: "CommandComment", + caspar_type: "template", + slide: 0, + slides: [{ + template: this.props.template, + data: this.props.data + }] + }); + }); + } + + navigate_slide(steps: number): number { + // return the steps, since there are no slides to navigate + return steps; + } + + set_active_slide(): number { + return 0; + } + + protected get_background_image(): Promise { + return new Promise((resolve) => resolve("")); + } + + get active_slide(): number { + return 0; + } + + get props(): CommandCommentProps { + return this.item_props; + } +} \ No newline at end of file diff --git a/src/server/SequenceItems/Image.ts b/src/server/SequenceItems/Image.ts index 83d7bd2..65cefe6 100644 --- a/src/server/SequenceItems/Image.ts +++ b/src/server/SequenceItems/Image.ts @@ -58,8 +58,7 @@ export default class Image extends SequenceItemBase { slide: 0, slides: [], file_name: this.props.FileName, - background_image: await this.get_background_image(proxy), - mute_transition: true + background_image: await this.get_background_image(proxy) }; } diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index 5cf1670..a85c1bc 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -6,8 +6,9 @@ import Song, { ClientSongSlides, SongProps, SongRenderObject } from "./Song"; import Countdown, { ClientCountdownSlides, CountdownProps, CountdownRenderObject } from "./Countdown"; import Comment, { ClientCommentSlides, CommentProps, CommentRenderObject } from "./Comment"; import Image, { ClientImageSlides, ImageProps, ImageRenderObject } from "./Image"; +import CommandComment, { ClientCommandCommentSlides, CommandCommentProps, CommandCommentRenderObject } from "./CommandComment"; -export type SequenceItem = Song | Countdown | Comment | Image; +export type SequenceItem = Song | Countdown | Comment | Image | CommandComment; export type DeepPartial = { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; @@ -28,7 +29,7 @@ export interface ItemPropsBase { /* eslint-enable @typescript-eslint/naming-convention */ } -export type ItemProps = SongProps | CountdownProps | CommentProps | ImageProps; +export type ItemProps = SongProps | CountdownProps | CommentProps | ImageProps | CommandCommentProps; type CasparGeneratorType = "media" | "template"; @@ -42,7 +43,7 @@ export interface ItemRenderObjectBase { mute_transition?: boolean; } -export type ItemRenderObject = SongRenderObject | CountdownRenderObject | CommentRenderObject | ImageRenderObject; +export type ItemRenderObject = SongRenderObject | CountdownRenderObject | CommentRenderObject | ImageRenderObject | CommandCommentRenderObject; export interface ClientItemSlidesBase { type: string; @@ -52,7 +53,7 @@ export interface ClientItemSlidesBase { slides_template: ItemRenderObject & { mute_transition: true; }; } -export type ClientItemSlides = ClientSongSlides | ClientCountdownSlides | ClientCommentSlides | ClientImageSlides; +export type ClientItemSlides = ClientSongSlides | ClientCountdownSlides | ClientCommentSlides | ClientImageSlides | ClientCommandCommentSlides; export interface FontFormat { /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/src/server/config.ts b/src/server/config.ts index a30bb89..c1ae1d6 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -2,7 +2,10 @@ export interface CasparCGConnectionSettings { host: string; port: number; channel: number; - layers: [number, number]; + layers: { + song: [number, number]; + command_comment: number; + }; } interface ConfigJSON { @@ -14,12 +17,7 @@ interface ConfigJSON { song: string; }; casparcg: { - templates: { - /* eslint-disable @typescript-eslint/naming-convention */ - Song: string, - Countdown: string - /* eslint-enable @typescript-eslint/naming-convention */ - }; + templates: string; connections: CasparCGConnectionSettings[]; }; client_server: { diff --git a/src/server/servers/http-server.ts b/src/server/servers/http-server.ts index 4dbce6b..3f5ce95 100644 --- a/src/server/servers/http-server.ts +++ b/src/server/servers/http-server.ts @@ -30,7 +30,7 @@ class HTTPServer { break; // serve the casparcg-templates case /^\/Templates\//.test(request.url): - resource_dir = "casparcg-templates/JohnCG"; + resource_dir = Config.casparcg.templates; request.url = request.url.replace(/\/Templates\//, ""); break; // serve the background-images From 74a2190c95caec47531b7f00677c05d55c40a0f7 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Sat, 24 Feb 2024 05:34:31 +0100 Subject: [PATCH 13/16] improved build script --- build.ps1 | 2 +- license-reporter.config.ts | 39 +++++++++++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/build.ps1 b/build.ps1 index 42f20ed..530837d 100644 --- a/build.ps1 +++ b/build.ps1 @@ -17,7 +17,7 @@ npm run build-client npm run build-templates # create sea-prep.blob -node --experimental-sea-config .\sea-config.json +# node --experimental-sea-config .\sea-config.json # get the node executable node -e "require('fs').copyFileSync(process.execPath, 'dist/build/$node_exec_name')" diff --git a/license-reporter.config.ts b/license-reporter.config.ts index 16698ac..6a7e395 100644 --- a/license-reporter.config.ts +++ b/license-reporter.config.ts @@ -1,13 +1,42 @@ import { IReporterConfiguration } from "@weichwarenprojekt/license-reporter"; export const configuration: Partial = { - defaultLicenseText: undefined, + // defaultLicenseText: undefined, output: "dist/build/3rdpartylicenses.json", overrides: [ { - name: "osc", - licenseName: "MIT", - licenseText: "Copyright (c) 2011-2014 Colin Clark\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." - } + name: "osc" + }, + { + name: "@esbuild/win32-x64" + }, + { + name: "binpack", + licenseName: "BSL-1.0 (modified)" + }, + { + name: "eastasianwidth" + }, + { + name: "esrecurse" + }, + { + name: "imurmurhash" + }, + { + name: "keyv" + }, + { + name: "natural-compare" + }, + { + name: "osc", + url: "https://github.com/colinbdclark/osc.js.git", + licenseName: "MIT", + licenseText: "Copyright (c) 2011-2014 Colin Clark\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + }, + { + name: "undici-types" + } ] }; \ No newline at end of file From 68328e1d1593430714f40e65f8aaa48c847aa760 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Sat, 24 Feb 2024 23:48:30 +0100 Subject: [PATCH 14/16] added visibility toggle over osc --- README.md | 4 +++- src/server/Sequence.ts | 27 +++++++++++++++++++++++++++ src/server/control.ts | 24 ++++++++++++++++++------ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 58cdd2a..cf12df1 100644 --- a/README.md +++ b/README.md @@ -28,4 +28,6 @@ Generate lyric-graphics and play them out through CasparCG. - client-messages: create message-log, group same - build-script in node / integrate with license-generator - command-comment: define commands / names which get loaded straigth from the start and can be shown anytime -- song: transmit template in server->client message instead of hardcoding it into the client \ No newline at end of file +- song: transmit template in server->client message instead of hardcoding it into the client +- fix "Buffer() is deprecated" +- cgplay after invisible: only foreground / send background as fast as possible dark \ No newline at end of file diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index 7f72ab9..dbf6771 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -534,6 +534,33 @@ class Sequence { } } + async toggle_visibility(): Promise { + this.casparcg_visibility = !this.visibility; + + for (const casparcg_connection of this.casparcg_connections) { + await casparcg_connection.connection.clear({ + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers.song[0] + }); + + const options = { + /* eslint-disable @typescript-eslint/naming-convention */ + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers.song[1], + cgLayer: 0 + /* eslint-enable @typescript-eslint/naming-convention */ + }; + + if (this.visibility) { + void casparcg_connection.connection.cgPlay(options); + } else { + void casparcg_connection.connection.cgStop(options); + } + } + + return this.visibility; + } + get active_item(): number { return this.active_item_number; } diff --git a/src/server/control.ts b/src/server/control.ts index 224d4f5..cd42894 100644 --- a/src/server/control.ts +++ b/src/server/control.ts @@ -26,7 +26,10 @@ class Control { } }, output: { - visibility: async (value: boolean) => this.set_visibility(value) + visibility: { + set: async (value: boolean) => this.set_visibility(value), + toggle: async (value: string) => this.toggle_visibility(value) + } } } }; @@ -203,6 +206,20 @@ class Control { } } + private async toggle_visibility(osc_feedback_path?: string): Promise { + let visibility_feedback = false; + + // if a sequence is loaded, get it's visibility + if (this.sequence !== undefined) { + visibility_feedback = await this.sequence.toggle_visibility(); + } + + // if a feedback-path is given, write the feedback to it + if (osc_feedback_path !== undefined) { + this.osc_server.send_value(osc_feedback_path, visibility_feedback); + } + } + /** * Send a JGCP-message to all registered clients * @param message JSON-message to be sent @@ -214,11 +231,6 @@ class Control { ws_clients.forEach((ws_client) => { ws_client.send(JSON.stringify(message)); }); - - // dummy-implementation-example - if (message.command === "state" && message.visibility !== undefined) { - this.osc_server.send_value("/custom-variable/johncg_visibility/value", message.visibility); - } } /** From 233944d066e19512709b3940695985e70ae88596 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Sun, 25 Feb 2024 03:50:34 +0100 Subject: [PATCH 15/16] seperated background-image into seperate media-layer and improved visibility function --- README.md | 2 +- casparcg-templates/JohnCG/Countdown.html | 6 - casparcg-templates/JohnCG/Song.html | 6 - client/main.css | 12 + config.json | 8 +- src/client/main.ts | 34 +- src/server/Sequence.ts | 342 ++++++++++----------- src/server/SequenceItems/CommandComment.ts | 16 +- src/server/SequenceItems/Countdown.ts | 25 +- src/server/SequenceItems/Image.ts | 2 + src/server/SequenceItems/SequenceItem.ts | 63 +++- src/server/SequenceItems/Song.ts | 35 ++- src/server/config.ts | 6 +- src/templates/Countdown.ts | 15 +- src/templates/Song.ts | 10 +- 15 files changed, 314 insertions(+), 268 deletions(-) diff --git a/README.md b/README.md index cf12df1..eef7cb6 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,4 @@ Generate lyric-graphics and play them out through CasparCG. - command-comment: define commands / names which get loaded straigth from the start and can be shown anytime - song: transmit template in server->client message instead of hardcoding it into the client - fix "Buffer() is deprecated" -- cgplay after invisible: only foreground / send background as fast as possible dark \ No newline at end of file +- generate background-image-b64 only if needed \ No newline at end of file diff --git a/casparcg-templates/JohnCG/Countdown.html b/casparcg-templates/JohnCG/Countdown.html index 380b029..ddb3ab9 100644 --- a/casparcg-templates/JohnCG/Countdown.html +++ b/casparcg-templates/JohnCG/Countdown.html @@ -4,12 +4,6 @@ font-size: 0.125vw; } - * { - cursor: pointer; - - user-select: none; - } - body { margin: 0px; diff --git a/casparcg-templates/JohnCG/Song.html b/casparcg-templates/JohnCG/Song.html index 0dbdd59..ba0f7af 100644 --- a/casparcg-templates/JohnCG/Song.html +++ b/casparcg-templates/JohnCG/Song.html @@ -11,12 +11,6 @@ JohnCG - Song-template