diff --git a/.gitignore b/.gitignore index 8bfc0b61..e1c20f98 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ node_modules assets.json dist bin +build # temporary files data \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c2425bc9..cef75057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,21 @@ - Support for named urls when sharing links to workspaces - Ability to convert items to other types by right-clicking - Ancestor items in context menus now stay tinted when their child menus are opened +- A shortcut to open preferences (CMD/CTRL+,) +- Support for lists in settings +- Support for custom ids in select inputs in settings +- Support for LTC timecode and triggers +- A state evaluation API +- Granular type inheritance +- Default names to types +### Changed +- Some features have moved to the footer of the app window +- Context menus now follow the color theme +- Windows builds now use a custom window header ### Fixed - An issue where the inspector started to scroll horisontally on overflow - Closing electron windows may cause a loop preventing user defaults from being saved +- An issue where settings didn't render after reload ## 1.0.0-beta.8 ### Fixed diff --git a/README.md b/README.md index ebc9e31a..0097eec5 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ The roadmap is available on Notion - OSC API and triggers - HTTP triggers - CasparCG library, playout and templates +- LTC timecode triggers ## Community plugins - [CRON - triggers based on the time of day](https://github.com/axelboberg/bridge-plugin-cron) diff --git a/api/events.js b/api/events.js index 0ee020f5..ab0b7e8f 100644 --- a/api/events.js +++ b/api/events.js @@ -202,7 +202,9 @@ class Events { * @param { EventHandler } handler A handler to remove */ off (event, handler) { - if (!this.localHandlers.has(event)) return + if (!this.localHandlers.has(event)) { + return + } const handlers = this.localHandlers.get(event) const index = handlers.findIndex(({ handler: _handler }) => _handler === handler) diff --git a/api/index.js b/api/index.js index 6b49c5cb..7be21204 100644 --- a/api/index.js +++ b/api/index.js @@ -18,6 +18,7 @@ require('./system') require('./state') require('./types') require('./items') +require('./time') require('./ui') class API { @@ -36,6 +37,7 @@ class API { this.state = props.State this.types = props.Types this.items = props.Items + this.time = props.Time this.ui = props.UI } } @@ -55,6 +57,7 @@ DIController.main.register('API', API, [ 'State', 'Types', 'Items', + 'Time', 'UI' ]) diff --git a/api/settings.js b/api/settings.js index 1c08d56d..9e12864b 100644 --- a/api/settings.js +++ b/api/settings.js @@ -12,6 +12,9 @@ const DIController = require('../shared/DIController') +const MissingArgumentError = require('./error/MissingArgumentError') +const InvalidArgumentError = require('./error/InvalidArgumentError') + class Settings { #props @@ -23,11 +26,31 @@ class Settings { * Register a setting * by its specification * @param { SettingSpecification } specification A setting specification - * @returns { Promise. } + * @returns { Promise. } */ registerSetting (specification) { return this.#props.Commands.executeCommand('settings.registerSetting', specification) } + + /** + * Apply changes to a registered + * setting in the state + * + * @param { String } id The id of a setting to update + * @param { SettingSpecification } set A setting object to apply + * @returns { Promise. } + */ + async applySetting (id, set = {}) { + if (typeof id !== 'string') { + throw new MissingArgumentError('Invalid value for item id, must be a string') + } + + if (typeof set !== 'object' || Array.isArray(set)) { + throw new InvalidArgumentError('Argument \'set\' must be a valid object that\'s not an array') + } + + return this.#props.Commands.executeCommand('settings.applySetting', id, set) + } } DIController.main.register('Settings', Settings, [ diff --git a/api/state.js b/api/state.js index bdc153ad..a136b362 100644 --- a/api/state.js +++ b/api/state.js @@ -7,8 +7,53 @@ const merge = require('../shared/merge') const Cache = require('./classes/Cache') const DIController = require('../shared/DIController') +const objectPath = require('object-path') + const CACHE_MAX_ENTRIES = 10 +function mapObject (obj, map) { + if (typeof map !== 'object' || typeof obj !== 'object') { + return obj + } + + const out = {} + for (const [key, path] of Object.entries(map)) { + if (typeof path !== 'string') { + continue + } + out[key] = obj[path] + } + return out +} + +const EVALUATION_OPERATIONS = { + arrayFromObject: (opts, data) => { + if (typeof opts?.path !== 'string') { + return + } + + const obj = objectPath.get(data, opts.path) + if (!obj) { + return + } + + return Object.entries(obj) + .map(([, value]) => { + let _value = value + if (typeof value !== 'object') { + _value = { value } + } + return mapObject(_value, opts?.map) + }) + }, + concatArrays: (opts, data) => { + if (!Array.isArray(opts?.a) || !Array.isArray(opts?.b)) { + return opts?.a + } + return [...opts.a, ...opts.b] + } +} + class State { #props @@ -117,24 +162,98 @@ class State { } } + /** + * Create a new object by expanding the path + * and set the provided value + * @param { string } path + * @param { any } value + * @param { string | undefined } delimiter + * @returns { any } + */ + #expandObjectPath (path, value, delimiter = '.') { + const parts = path.split(delimiter) + + const out = {} + let pointer = out + + for (let i = 0; i < parts.length; i++) { + const key = parts[i] + if (i === parts.length - 1) { + pointer[key] = value + } else { + pointer[key] = {} + pointer = pointer[key] + } + } + + return out + } + /** * Apply some data to the state, * most often this function shouldn't * be called directly - there's probably * a command for what you want to do - * @param { Object } set Data to apply to the state + * @param { object } set Data to apply to the state *//** * Apply some data to the state, * most often this function shouldn't * be called directly - there's probably * a command for what you want to do - * @param { Object[] } set An array of data objects to + * @param { object[] } set An array of data objects to * apply to the state in order + *//** + * Apply some data to the state, + * most often this function shouldn't + * be called directly - there's probably + * a command for what you want to do + * @param { string } path A dot-path to which the value will be applied + * @param { object } set A value to apply */ - apply (set) { + apply (arg0, arg1) { + let set = arg0 + + /* + If the function received a path and a value, + expand create an object that can be set directly + */ + if (typeof arg0 === 'string' && arg1) { + set = this.#expandObjectPath(arg0, arg1) + } + this.#props.Commands.executeRawCommand('state.apply', set) } + #evaluateProperty (propertyFieldEvaluation, dataDict = {}) { + const op = propertyFieldEvaluation?.op + if (!op || !EVALUATION_OPERATIONS[op]) { + return + } + return EVALUATION_OPERATIONS[op](propertyFieldEvaluation, dataDict) + } + + async evaluate (obj, data) { + if (typeof obj !== 'object' || !obj) { + return obj + } + + let _data = data + if (!_data) { + _data = this.getLocalState() || await this.get() + } + + for (const key of Object.keys(obj)) { + obj[key] = await this.evaluate(obj[key], _data) + } + + if (obj?.$eval) { + const newObj = this.#evaluateProperty(obj.$eval, _data) + return this.evaluate(newObj, _data) + } + + return obj + } + /** * Get the full current state * @returns { Promise. } diff --git a/api/time.js b/api/time.js new file mode 100644 index 00000000..24ba6ffc --- /dev/null +++ b/api/time.js @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 Axel Boberg +// +// SPDX-License-Identifier: MIT + +const DIController = require('../shared/DIController') + +class Time { + #props + + constructor (props) { + this.#props = props + } + + getAllClocks () { + return this.#props.Commands.executeCommand('time.getAllClocks') + } + + registerClock (spec) { + return this.#props.Commands.executeCommand('time.registerClock', spec) + } + + removeClock (id) { + return this.#props.Commands.executeCommand('time.removeClock', id) + } + + applyClock (id, set) { + return this.#props.Commands.executeCommand('time.applyClock', id, set) + } + + submitFrame (id, frame) { + return this.#props.Commands.executeRawCommand('time.submitFrame', id, frame) + } +} + +DIController.main.register('Time', Time, [ + 'State', + 'Events', + 'Commands' +]) diff --git a/api/types.js b/api/types.js index be3549c5..ea65ad54 100644 --- a/api/types.js +++ b/api/types.js @@ -5,21 +5,50 @@ const Cache = require('./classes/Cache') const DIController = require('../shared/DIController') +const utils = require('./utils') + const CACHE_MAX_ENTRIES = 100 +function shallowMergeObjects (a, b) { + if (typeof a !== 'object' || typeof b !== 'object') { + return b + } + + return { + ...a, + ...b + } +} + +/* +Export for testing only +*/ +exports.shallowMergeObjects = shallowMergeObjects + /** - * Perform a deep clone - * of an object - * @param { any } obj An object to clone + * Merge all properties two level deep + * from two types + * @param { any } a + * @param { any } b * @returns { any } */ -function deepClone (obj) { - if (typeof window !== 'undefined' && window.structuredClone) { - return window.structuredClone(obj) +function mergeProperties (a, b) { + const out = { ...a } + for (const key of Object.keys(b)) { + if (Object.prototype.hasOwnProperty.call(out, key)) { + out[key] = shallowMergeObjects(a[key], b[key]) + } else { + out[key] = b[key] + } } - return JSON.parse(JSON.stringify(obj)) + return out } +/* +Export for testing only +*/ +exports.mergeProperties = mergeProperties + class Types { #props @@ -43,7 +72,8 @@ class Types { renderType (id, typesDict = {}) { if (!typesDict[id]) return undefined - const type = deepClone(typesDict[id]) + const type = utils.deepClone(typesDict[id]) + type.ancestors = [] /* Render the ancestor if this @@ -52,10 +82,12 @@ class Types { if (type.inherits) { const ancestor = this.renderType(type.inherits, typesDict) - type.properties = { - ...ancestor?.properties || {}, - ...type.properties || {} - } + type.ancestors = [...(ancestor?.ancestors || []), type.inherits] + type.category = type.category || ancestor?.category + type.properties = mergeProperties( + (ancestor?.properties || {}), + (type?.properties || {}) + ) } return type diff --git a/api/types.unit.test.js b/api/types.unit.test.js new file mode 100644 index 00000000..b40e0d9b --- /dev/null +++ b/api/types.unit.test.js @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Sveriges Television AB +// +// SPDX-License-Identifier: MIT + +require('./variables') + +const DIController = require('../shared/DIController') +const types = require('./types') + +test('merge properties', () => { + const typeAProperties = { + foo: { + type: 'string', + default: 'bar' + }, + bar: { + type: 'number', + default: 2 + } + } + + const typeBProperties = { + foo: { + default: 'baz' + }, + qux: { + type: 'string', + default: 'foo' + } + } + + expect(types.mergeProperties(typeAProperties, typeBProperties)).toMatchObject({ + foo: { + type: 'string', + default: 'baz' + }, + bar: { + type: 'number', + default: 2 + }, + qux: { + type: 'string', + default: 'foo' + } + }) +}) diff --git a/api/utils.js b/api/utils.js new file mode 100644 index 00000000..2afa0bed --- /dev/null +++ b/api/utils.js @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 Sveriges Television AB +// +// SPDX-License-Identifier: MIT + +/** + * Perform a deep clone + * of an object + * @param { any } obj An object to clone + * @returns { any } + */ +function deepClone (obj) { + if (typeof window !== 'undefined' && window.structuredClone) { + return window.structuredClone(obj) + } + return JSON.parse(JSON.stringify(obj)) +} +exports.deepClone = deepClone diff --git a/app/App.jsx b/app/App.jsx index 20ce7b6e..1ae17d05 100644 --- a/app/App.jsx +++ b/app/App.jsx @@ -76,6 +76,7 @@ root html tag for platform-specific styling e.t.c. const token = await auth.getToken() const bridge = await api.load() bridge.commands.setHeader('authentication', token) + bridge.events.emitLocally('didSetAuthenticationHeader') })() async function updateControlsColors () { diff --git a/app/assets/icons/index.js b/app/assets/icons/index.js index d2e8b57c..747ae5d3 100644 --- a/app/assets/icons/index.js +++ b/app/assets/icons/index.js @@ -10,9 +10,12 @@ import person from './person.svg' import reload from './reload.svg' import search from './search.svg' import widget from './widget.svg' +import spinner from './spinner.svg' +import trigger from './trigger.svg' import rundown from './rundown.svg' import warning from './warning.svg' import selector from './selector.svg' +import variable from './variable.svg' import inspector from './inspector.svg' import arrowRight from './arrow-right.svg' import editDetail from './edit-detail.svg' @@ -36,9 +39,12 @@ export default { reload, search, widget, + spinner, + trigger, rundown, warning, selector, + variable, inspector, arrowRight, editDetail, diff --git a/app/assets/icons/spinner.svg b/app/assets/icons/spinner.svg index 735cd89d..dbfc8d70 100644 --- a/app/assets/icons/spinner.svg +++ b/app/assets/icons/spinner.svg @@ -1,7 +1,27 @@ icons/spinner - + + \ No newline at end of file diff --git a/app/assets/icons/trigger.svg b/app/assets/icons/trigger.svg new file mode 100644 index 00000000..6c601ffb --- /dev/null +++ b/app/assets/icons/trigger.svg @@ -0,0 +1,7 @@ + + + icons/trigger + + + + \ No newline at end of file diff --git a/app/assets/icons/variable.svg b/app/assets/icons/variable.svg new file mode 100644 index 00000000..0db0a20c --- /dev/null +++ b/app/assets/icons/variable.svg @@ -0,0 +1,8 @@ + + + icons/variable + + + + + \ No newline at end of file diff --git a/app/bridge.css b/app/bridge.css index 318e602a..16ae5352 100644 --- a/app/bridge.css +++ b/app/bridge.css @@ -74,7 +74,6 @@ button { .Button--accent, .Button--primary, .Button--secondary, -.Button--tertiary, .Button--ghost { padding: 0.9em 1.5em; @@ -200,20 +199,23 @@ input[type="search"] { input[type="checkbox"] { display: inline-block; position: relative; - padding: 0.2em 0; + padding: 3px 0; width: 40px; - height: 1.1em; + height: 22px; margin-right: 10px; - background: var(--base-color--grey1); border-radius: 50px; - vertical-align: bottom; + box-shadow: inset 0 0 0 1px var(--base-color--shade); + + vertical-align: middle; -webkit-appearance: none; appearance: none; + + overflow: hidden; } input[type="checkbox"]::before { @@ -222,25 +224,47 @@ input[type="checkbox"]::before { top: 50%; left: 0; - width: 1.5em; - height: 1.5em; + width: 17px; + height: 17px; - background: var(--base-color--grey3); + background: var(--base-color); border-radius: 50px; - box-shadow: 0 0 0 2px var(--base-color--background); + vertical-align: middle; + + transform: translate(3px, -50%); + transition: 0.2s; + + opacity: 0.7; + z-index: 1; +} + +input[type="checkbox"]:checked::after { + content: ""; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + + background: var(--base-color--accent1); + opacity: 0; - transform: translate(0, -50%); transition: 0.2s; } -input[type="checkbox"]:active::before { - background: var(--base-color--grey3); +input[type="checkbox"]:focus { + box-shadow: inset 0 0 0 1px var(--base-color--shade), 0 0 0 1px var(--base-color--shade); +} + +input[type="checkbox"]:checked::after { + opacity: 0.2; } input[type="checkbox"]:checked::before { background: var(--base-color--accent1); - transform: translate(100%, -50%); + transform: translate(20px, -50%); + opacity: 1; } select { @@ -337,21 +361,6 @@ select:focus { mask-repeat: no-repeat; mask-image: url('./assets/icons/spinner.svg'); mask-size: 90%; - - animation-name: Loader-spinner; - animation-duration: 750ms; - animation-fill-mode: forwards; - animation-timing-function: linear; - animation-iteration-count: infinite; -} - -@keyframes Loader-spinner { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } } /* Warning */ diff --git a/app/components/AppMenu/AppMenuRootItem.css b/app/components/AppMenu/AppMenuRootItem.css index 42ff7e11..5070ac74 100644 --- a/app/components/AppMenu/AppMenuRootItem.css +++ b/app/components/AppMenu/AppMenuRootItem.css @@ -1,6 +1,7 @@ .AppMenuRootItem { - padding: 0.3em 0.8em; + padding: 0.4em 0.8em; border-radius: 7px; + font-size: 0.9em; } .AppMenuRootItem:hover, diff --git a/app/components/AppMenu/index.jsx b/app/components/AppMenu/index.jsx index 738b224a..167674a7 100644 --- a/app/components/AppMenu/index.jsx +++ b/app/components/AppMenu/index.jsx @@ -25,13 +25,33 @@ export function AppMenu () { const [menu, setMenu] = React.useState() React.useEffect(() => { - async function getMenu () { - const bridge = await api.load() + let bridge + + async function updateMenu () { + if (!bridge) { + return + } const menu = await bridge.commands.executeCommand('window.getAppMenu') recursivePopulateCommandsInPlace(menu) setMenu(menu) } - getMenu() + + async function setup () { + bridge = await api.load() + bridge.events.on('didSetAuthenticationHeader', updateMenu) + + if (bridge.commands.getHeader('authentication')) { + updateMenu() + } + } + setup() + + return () => { + if (!bridge) { + return + } + bridge.events.off('didSetAuthenticationHeader', updateMenu) + } }, []) return ( diff --git a/app/components/AppMenu/style.css b/app/components/AppMenu/style.css index 3371d255..6e60ce82 100644 --- a/app/components/AppMenu/style.css +++ b/app/components/AppMenu/style.css @@ -2,5 +2,7 @@ display: flex; font-size: 0.9em; + margin-left: -15px; + -webkit-app-region: no-drag; } \ No newline at end of file diff --git a/app/components/ContextMenu/index.jsx b/app/components/ContextMenu/index.jsx index c51e4e55..2b0ac665 100644 --- a/app/components/ContextMenu/index.jsx +++ b/app/components/ContextMenu/index.jsx @@ -90,7 +90,7 @@ export const ContextMenu = ({ x, y, width = DEFAULT_WIDTH_PX, children, onClose createPortal(
{children} diff --git a/app/components/ContextMenu/style.css b/app/components/ContextMenu/style.css index fb362d07..acef6788 100644 --- a/app/components/ContextMenu/style.css +++ b/app/components/ContextMenu/style.css @@ -1,13 +1,15 @@ .ContextMenu { position: fixed; - background: white; - color: black; + background: var(--base-color--background1); + color: var(--base-color); border-radius: 5px; box-sizing: border-box; box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + + border: 1px solid var(--base-color--shade); margin-top: -3px; diff --git a/app/components/ContextMenuDivider/style.css b/app/components/ContextMenuDivider/style.css index aac31d32..12ddd705 100644 --- a/app/components/ContextMenuDivider/style.css +++ b/app/components/ContextMenuDivider/style.css @@ -2,6 +2,6 @@ width: 100%; height: 1px; - background: black; - opacity: 0.1; + background: var(--base-color--shade3); + opacity: 0.3; } diff --git a/app/components/ContextMenuItem/index.jsx b/app/components/ContextMenuItem/index.jsx index 082a271e..9ef810c1 100644 --- a/app/components/ContextMenuItem/index.jsx +++ b/app/components/ContextMenuItem/index.jsx @@ -9,7 +9,7 @@ import { Icon } from '../Icon' * nested context menus * @type { Number } */ -const CTX_MENU_OFFSET_X_PX = 150 +const CTX_MENU_OFFSET_X_PX = 140 /** * A delay applied to when the @@ -77,7 +77,7 @@ export const ContextMenuItem = ({ text, children = [], onClick = () => {} }) =>
{ childArr.length > 0 && - + } { delayedHover && childArr.length > 0 diff --git a/app/components/ContextMenuItem/style.css b/app/components/ContextMenuItem/style.css index 6b5af8e7..66fd4641 100644 --- a/app/components/ContextMenuItem/style.css +++ b/app/components/ContextMenuItem/style.css @@ -11,7 +11,7 @@ padding: 0.4em 0.5em; border-radius: 4px; - font-size: 0.95em; + font-size: 0.9em; text-overflow: ellipsis; white-space: nowrap; @@ -25,15 +25,13 @@ right: 7px; transform: translate(0, -50%); - - --base-color: black; } .ContextMenuItem:hover > .ContextMenuItem-text, .ContextMenuItem.is-hovered > .ContextMenuItem-text { - background: rgb(228, 228, 228); + background: var(--base-color--shade); } .ContextMenuItem:active > .ContextMenuItem-text { - background: rgb(211, 211, 211); + box-shadow: 2px solid var(--base-color--shade3); } \ No newline at end of file diff --git a/app/components/Footer/index.jsx b/app/components/Footer/index.jsx new file mode 100644 index 00000000..a5cc15de --- /dev/null +++ b/app/components/Footer/index.jsx @@ -0,0 +1,74 @@ +import React from 'react' + +import { SharedContext } from '../../sharedContext' +import { LocalContext } from '../../localContext' + +import { Role } from '../Role' +import { Modal } from '../Modal' +import { AppMenu } from '../AppMenu' +import { Palette } from '../Palette' +import { Sharing } from '../Sharing' +import { Preferences } from '../Preferences' + +import { Icon } from '../Icon' + +import * as api from '../../api' +import * as windowUtils from '../../utils/window' + +import './style.css' + +const DEFAULT_TITLE = 'Unnamed' + +function handleReload () { + window.location.reload() +} + +export function Footer ({ title = DEFAULT_TITLE, features }) { + const [shared, applyShared] = React.useContext(SharedContext) + const [local] = React.useContext(LocalContext) + + const [sharingOpen, setSharingOpen] = React.useState(false) + const [roleOpen, setRoleOpen] = React.useState(false) + + const connectionCount = Object.keys(shared?._connections || {}).length + const role = shared?._connections?.[local.id]?.role + + function featureShown (feature) { + if (!Array.isArray(features)) { + return true + } + return features.includes(feature) + } + + return ( + <> +
+
+ { + featureShown('role') && + ( +
+ + setRoleOpen(false)} /> +
+ ) + } + { + featureShown('sharing') && + ( +
+ + setSharingOpen(false)} /> +
+ ) + } +
+
+ + ) +} diff --git a/app/components/Footer/style.css b/app/components/Footer/style.css new file mode 100644 index 00000000..e131ec1f --- /dev/null +++ b/app/components/Footer/style.css @@ -0,0 +1,96 @@ +.Footer { + position: sticky; + display: flex; + bottom: 0; + width: 100%; + height: 24px; + + border-top: 1px solid var(--base-color--shade); + box-sizing: border-box; + + flex-shrink: 0; + + align-items: center; + justify-content: space-between; +} + +.Footer-block { + display: flex; + align-items: center; +} + +.Footer-actionSection { + margin-right: 10px; +} + +.Footer-button.Footer-sharingBtn { + width: 40px; + padding: 0 1.3em 0 1em; + color: var(--base-color); + font-family: var(--base-fontFamily--primary); +} + +.Footer-button.Footer-roleBtn { + width: auto; + padding: 0 7px 0 0; + + white-space: nowrap; + overflow: hidden; +} + +.Footer-button.Footer-roleBtn::before { + content: ''; + height: 100%; + width: 12px; + + margin-right: 7px; + + background: var(--base-color--shade); +} + +.Footer-button.Footer-roleBtn.is-main::before { + background: var(--base-color--accent1); +} + +.Footer-button { + display: flex; + position: relative; + min-width: 32px; + height: 24px; + + border: none; + + color: inherit; + font-size: 0.8em; + font-family: var(--base-fontFamily--primary); + + background: none; + + align-items: center; + justify-content: center; + + -webkit-app-region: no-drag; + + overflow: hidden; +} + +.Footer-button:not(.is-active):hover { + background: var(--base-color--shade); + opacity: 0.7; +} + +.Footer-button.is-active::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background: var(--base-color--accent1); + opacity: 0.15; +} + +.Footer-button.is-active:hover { + opacity: 0.7; +} \ No newline at end of file diff --git a/app/components/FrameComponent/index.jsx b/app/components/FrameComponent/index.jsx index e36bc12e..f2d62cb8 100644 --- a/app/components/FrameComponent/index.jsx +++ b/app/components/FrameComponent/index.jsx @@ -2,7 +2,6 @@ import React from 'react' import { v4 as uuidv4 } from 'uuid' -import { SharedContext } from '../../sharedContext' import { LocalContext } from '../../localContext' import { Icon } from '../Icon' @@ -91,10 +90,8 @@ function copyThemeVariables (iframe, variables = COPY_THEME_VARIABLES) { } } -export function FrameComponent ({ data, onUpdate, enableFloat = true }) { +export function FrameComponent ({ widgetId, uri, widgets, data, onUpdate, enableFloat = true }) { const [caller] = React.useState(uuidv4()) - - const [shared] = React.useContext(SharedContext) const [local] = React.useContext(LocalContext) const [hasFocus, setHasFocus] = React.useState(false) @@ -103,6 +100,11 @@ export function FrameComponent ({ data, onUpdate, enableFloat = true }) { const wrapperRef = React.useRef() const frameRef = React.useRef() + const onUpdateRef = React.useRef() + React.useEffect(() => { + onUpdateRef.current = onUpdate + }, [onUpdate]) + React.useEffect(() => { async function setup () { const bridge = await api.load() @@ -139,7 +141,7 @@ export function FrameComponent ({ data, onUpdate, enableFloat = true }) { removal */ frameRef.current.contentWindow.WIDGET_UPDATE = set => { - onUpdate(set) + onUpdateRef.current(set) } /* @@ -163,14 +165,19 @@ export function FrameComponent ({ data, onUpdate, enableFloat = true }) { } } - const uri = shared?._widgets?.[data.component]?.uri + /* + Prevent re-mounting the iframe + unless absolutely necessary + Without this check, the iframe would reload + every time the user changes the layout + */ const snapshot = JSON.stringify([data, uri]) if (snapshot === snapshotRef.current) return snapshotRef.current = snapshot setup() - }, [data, shared, onUpdate]) + }, [uri, data]) /* Clean up all event listeners @@ -183,7 +190,7 @@ export function FrameComponent ({ data, onUpdate, enableFloat = true }) { bridge.events.removeAllListeners(caller) bridge.events.removeAllIntercepts(caller) } - }, [caller, shared?._widgets?.[data.component]?.uri]) + }, [caller, uri]) /* Highligh the component @@ -264,12 +271,12 @@ export function FrameComponent ({ data, onUpdate, enableFloat = true }) {
- {shared?._widgets?.[data.component]?.name} + {widgets?.[data.component]?.name}
{ - enableFloat && shared?._widgets?.[data.component]?.supportsFloat && - } diff --git a/app/components/Grid/index.jsx b/app/components/Grid/index.jsx index 483253a9..cf91ecd8 100644 --- a/app/components/Grid/index.jsx +++ b/app/components/Grid/index.jsx @@ -214,7 +214,7 @@ export function Grid ({ children, data = {}, onChange }) { } /** - * Render the correnct context menu + * Render the correct context menu * based on data from the event */ function renderContextMenu (x, y, data) { diff --git a/app/components/Header/index.jsx b/app/components/Header/index.jsx index 62727865..e0daf4f4 100644 --- a/app/components/Header/index.jsx +++ b/app/components/Header/index.jsx @@ -3,11 +3,9 @@ import React from 'react' import { SharedContext } from '../../sharedContext' import { LocalContext } from '../../localContext' -import { Role } from '../Role' import { Modal } from '../Modal' import { AppMenu } from '../AppMenu' import { Palette } from '../Palette' -import { Sharing } from '../Sharing' import { Preferences } from '../Preferences' import { Icon } from '../Icon' @@ -28,13 +26,9 @@ export function Header ({ title = DEFAULT_TITLE, features }) { const [local] = React.useContext(LocalContext) const [paletteIsOpen, setPaletteIsOpen] = React.useState(false) - const [sharingOpen, setSharingOpen] = React.useState(false) const [prefsOpen, setPrefsOpen] = React.useState(false) - const [roleOpen, setRoleOpen] = React.useState(false) - const connectionCount = Object.keys(shared?._connections || {}).length const isEditingLayout = shared?._connections?.[local?.id]?.isEditingLayout - const role = shared?._connections?.[local.id]?.role /* Listen for shortcuts @@ -45,6 +39,10 @@ export function Header ({ title = DEFAULT_TITLE, features }) { switch (shortcut) { case 'openPalette': setPaletteIsOpen(true) + break + case 'openSettings': + setPrefsOpen(true) + break } } @@ -143,29 +141,6 @@ export function Header ({ title = DEFAULT_TITLE, features }) { ) }
- { - featureShown('role') && - ( -
- - setRoleOpen(false)} /> -
- ) - } - { - featureShown('sharing') && - ( -
- - setSharingOpen(false)} /> -
- ) - } { featureShown('palette') && ( diff --git a/app/components/Header/style.css b/app/components/Header/style.css index f8568895..c1046d64 100644 --- a/app/components/Header/style.css +++ b/app/components/Header/style.css @@ -31,7 +31,7 @@ for the traffic light Do the same for Windows */ .Header.has-rightMargin { - padding-right: 180px; + padding-right: 150px; } .Header-title { @@ -55,41 +55,6 @@ Do the same for Windows align-items: center; } -.Header-actionSection { - margin-right: 55px; -} - -.Header-button.Header-sharingBtn { - width: 40px; - padding: 0 1.3em 0 1em; - color: var(--base-color); - font-family: var(--base-fontFamily--primary); -} - -.Header-button.Header-roleBtn { - width: auto; - padding: 0 7px 0 0; - - border: 1px solid var(--base-color--shade); - - white-space: nowrap; - overflow: hidden; -} - -.Header-button.Header-roleBtn::before { - content: ''; - height: 100%; - width: 6px; - - margin-right: 7px; - - background: var(--base-color--shade); -} - -.Header-button.Header-roleBtn.is-main::before { - background: var(--base-color--accent1); -} - .Header-button { display: flex; position: relative; diff --git a/app/components/Modal/index.jsx b/app/components/Modal/index.jsx index bb496b8e..b5a05dfe 100644 --- a/app/components/Modal/index.jsx +++ b/app/components/Modal/index.jsx @@ -5,6 +5,7 @@ import { useDraggable } from '../../hooks/useDraggable' import './style.css' import * as modalStack from '../../utils/modals' +import { createPortal } from 'react-dom' export function Modal ({ children, open, size = 'large', onClose = () => {}, draggable = false, shade = true }) { const elRef = React.useRef() @@ -31,15 +32,24 @@ export function Modal ({ children, open, size = 'large', onClose = () => {}, dra }, [open]) return ( -
-
-
- { - draggable &&
•••
- } - {children} -
-
-
+ <> + { + createPortal( + ( +
+
+
+ { + draggable &&
•••
+ } + {children} +
+
+
+ ), + document.body + ) + } + ) } diff --git a/app/components/Modal/style.css b/app/components/Modal/style.css index 6c1365a8..2d2561e2 100644 --- a/app/components/Modal/style.css +++ b/app/components/Modal/style.css @@ -59,6 +59,11 @@ slight box shadow instead max-height: 500px; } +.Modal--auto .Modal-wrapper { + width: auto; + height: auto; +} + .Modal-content { display: block; position: relative; diff --git a/app/components/Notification/style.css b/app/components/Notification/style.css index 1d1429fa..d12986e5 100644 --- a/app/components/Notification/style.css +++ b/app/components/Notification/style.css @@ -82,6 +82,15 @@ --color-text: black; } +.Notification--info { + background-color: var(--base-color--background1); +} + +.Notification--info *, +.Notification--info .Notification-hideBtn { + --color-text: var(--base-color); +} + .Notification-hideBtn { color: var(--color-text); } diff --git a/app/components/Popover/index.jsx b/app/components/Popover/index.jsx index 92c9efb1..a2bfe846 100644 --- a/app/components/Popover/index.jsx +++ b/app/components/Popover/index.jsx @@ -2,7 +2,18 @@ import React from 'react' import './style.css' -export function Popover ({ children, open = false, onClose = () => {} }) { +const ALIGNMENT = { + center: '-50%', + left: '0', + right: '-100%' +} + +const DIRECTION = { + up: 'calc(-100% - 10px)', + down: '0' +} + +export function Popover ({ children, open = false, direction = 'down', alignment = 'center', onClose = () => {} }) { const elRef = React.useRef() React.useEffect(() => { @@ -31,11 +42,13 @@ export function Popover ({ children, open = false, onClose = () => {} }) { }, [onClose]) return ( -
+
{ open && ( -
+
{children}
) diff --git a/app/components/Popover/style.css b/app/components/Popover/style.css index 83cf7cf1..b3a4855b 100644 --- a/app/components/Popover/style.css +++ b/app/components/Popover/style.css @@ -7,9 +7,6 @@ display: block; position: absolute; - left: 50%; - bottom: 0; - text-align: center; border-radius: 15px; @@ -18,8 +15,6 @@ box-sizing: border-box; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); - transform: translate(-50%, 100%); - overflow: hidden; z-index: 1; } diff --git a/app/components/Preferences/index.jsx b/app/components/Preferences/index.jsx index 15868bd2..341c2749 100644 --- a/app/components/Preferences/index.jsx +++ b/app/components/Preferences/index.jsx @@ -17,6 +17,8 @@ import state from './sections/state.json' import './style.css' +import * as api from '../../api' + /** * Internal settings defined * by json files @@ -40,52 +42,69 @@ const INTERNAL_SETTINGS = [ ] export function Preferences ({ onClose = () => {} }) { - const [shared, applyShared] = React.useContext(SharedContext) + const [, applyShared] = React.useContext(SharedContext) const [, applyLocal] = React.useContext(LocalContext) - const pluginSettings = React.useRef({ - title: 'Plugins', - items: [] - }) - - /** - * All plugin sections - * aggregated together - */ - const sections = [ - ...INTERNAL_SETTINGS, - pluginSettings.current - ] - - const [section, setSection] = React.useState(sections[0]?.items[0]) + const [sections, setSections] = React.useState([]) + const [curPath, setCurPath] = React.useState([0, 0]) /* - Append settings from the state - to the plugins section on component - load + List plugins from the + state whenever it's updated */ React.useEffect(() => { - Object.entries(shared?._settings || {}) - /* - Sort the groups alphabetically to - always keep the same order - */ - .sort((a, b) => a[0].localeCompare(b[0])) - .forEach(([groupName, settings]) => { - pluginSettings.current.items.push({ - title: groupName, - items: settings + function updatePluginSettings (settings) { + const pluginSections = Object.entries(settings || {}) + /* + Sort the groups alphabetically to + always keep the same order + */ + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([groupName, groupSettings]) => { + return { + title: groupName, + items: groupSettings + } }) - }) + + setSections([ + ...INTERNAL_SETTINGS, + { + title: 'Plugins', + items: pluginSections + } + ]) + } + + function onStateChange (newState, set) { + if (!set.hasOwnProperty('_settings')) { + return + } + updatePluginSettings(newState?._settings) + } + + let bridge + async function setup () { + bridge = await api.load() + bridge.events.on('state.change', onStateChange) + + const initialSettings = await bridge.state.get('_settings') + updatePluginSettings(initialSettings) + } + setup() return () => { - pluginSettings.current.items = [] + setPluginSections([]) + + if (!bridge?.events) { + return + } + bridge.events.off('state.change', onStateChange) } - }, [shared._settings]) + }, []) function handleSidebarClick (path) { - const pane = sections[path[0]]?.items[path[1]] - setSection(pane) + setCurPath([path[0], path[1]]) } function handleCloseClick () { @@ -133,13 +152,14 @@ export function Preferences ({ onClose = () => {} }) {
{ - (section?.items || []) + (sections[curPath[0]]?.items[curPath[1]]?.items || []) + .filter(setting => setting) .map(setting => { /* Compose a key that's somewhat unique but still static in order to prevent unnecessary re-rendering */ - const key = `${setting.title}${setting.description}${JSON.stringify(setting.inputs)}` + const key = `${setting?.title}${setting?.description}${JSON.stringify(setting?.inputs)}` return ( {} }) { +export function Preference ({ setting, values, onChange = () => {} }) { const [shared] = React.useContext(SharedContext) const [local] = React.useContext(LocalContext) @@ -22,10 +22,15 @@ export function Preference ({ setting, onChange = () => {} }) { */ function valueFromPath (path) { const parts = path.split('.') - const sourceName = parts.shift() + const firstKey = parts.shift() - let source = local - if (sourceName === 'shared') { + let source = values || local + + if (parts.length === 0) { + return source[firstKey] + } + + if (firstKey === 'shared') { source = shared } @@ -51,12 +56,12 @@ export function Preference ({ setting, onChange = () => {} }) { { (setting.inputs || []) .filter(input => inputComponents[input.type]) - .map((input, i) => { + .map(input => { const InputComponent = inputComponents[input.type] const bind = `${setting.bind ? `${setting.bind}.` : ''}${input.bind ?? ''}` return ( onChange(bind, value)} diff --git a/app/components/Preferences/shared.js b/app/components/Preferences/shared.js index ac6fdb84..f3428ef3 100644 --- a/app/components/Preferences/shared.js +++ b/app/components/Preferences/shared.js @@ -3,11 +3,13 @@ import { PreferencesSegmentedInput } from '../PreferencesSegmentedInput' import { PreferencesShortcutsInput } from '../PreferencesShortcutsInput' import { PreferencesVersionInput } from '../PreferencesVersionInput' import { PreferencesBooleanInput } from '../PreferencesBooleanInput' -import { PreferencesStringInput } from '../PreferencesStringInput' +import { PreferencesButtonInput } from '../PreferencesButtonInput' import { PreferencesNumberInput } from '../PreferencesNumberInput' import { PreferencesSelectInput } from '../PreferencesSelectInput' +import { PreferencesStringInput } from '../PreferencesStringInput' import { PreferencesThemeInput } from '../PreferencesThemeInput' import { PreferencesFrameInput } from '../PreferencesFrameInput' +import { PreferencesListInput } from '../PreferencesListInput' /** * Map typenames to components @@ -19,10 +21,12 @@ export const inputComponents = { segmented: PreferencesSegmentedInput, boolean: PreferencesBooleanInput, version: PreferencesVersionInput, - string: PreferencesStringInput, + button: PreferencesButtonInput, number: PreferencesNumberInput, select: PreferencesSelectInput, + string: PreferencesStringInput, theme: PreferencesThemeInput, frame: PreferencesFrameInput, - clear: PreferencesClearStateInput + clear: PreferencesClearStateInput, + list: PreferencesListInput } diff --git a/app/components/PreferencesButtonInput/index.jsx b/app/components/PreferencesButtonInput/index.jsx new file mode 100644 index 00000000..9877cd8e --- /dev/null +++ b/app/components/PreferencesButtonInput/index.jsx @@ -0,0 +1,33 @@ +import React from 'react' +import './style.css' + +import { Icon } from '../Icon' + +import * as api from '../../api' + +export function PreferencesButtonInput ({ label, buttonText, buttonIsLoading, command }) { + async function handleButtonClick () { + if (!command || typeof command !== 'string') { + return + } + const bridge = await api.load() + bridge.commands.executeCommand(command) + } + + return ( +
+
+
+ + { + buttonIsLoading && + ( +
+ +
+ ) + } +
+
+ ) +} diff --git a/app/components/PreferencesButtonInput/style.css b/app/components/PreferencesButtonInput/style.css new file mode 100644 index 00000000..d3ccb380 --- /dev/null +++ b/app/components/PreferencesButtonInput/style.css @@ -0,0 +1,25 @@ +.PreferencesButtonInput { + margin-top: 10px; +} + +.PreferencesButtonInput label { + display: inline-block; + margin-bottom: 10px; +} + +.PreferencesButtonInput-wrapper { + display: flex; + align-items: center; +} + +.PreferencesButtonInput-loader { + display: inline-block; + width: 30px; + height: 30px; + margin-left: 10px; +} + +.PreferencesButtonInput-loader svg { + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/app/components/PreferencesListInput/index.jsx b/app/components/PreferencesListInput/index.jsx new file mode 100644 index 00000000..08eb97b2 --- /dev/null +++ b/app/components/PreferencesListInput/index.jsx @@ -0,0 +1,106 @@ +import React from 'react' +import './style.css' + +import * as uuid from 'uuid' + +import { Preference } from '../Preferences/preference' + +function PreferencesListInputItem ({ value = {}, settings = [], onChange = () => {}, onDelete = () => {} }) { + function handleChange (key, newValue) { + onChange({ + ...value, + [key]: newValue + }) + } + + function handleDelete () { + onDelete() + } + + return ( +
+
+
+ { + (Array.isArray(settings) ? settings : []) + .map(setting => { + return ( +
+ +
+ ) + }) + } +
+
+ +
+
+
+ ) +} + +export function PreferencesListInput ({ label, value = [], settings = [], onChange = () => {}, buttonTextCreate = 'New' }) { + const items = Array.isArray(value) ? value : [] + + function handleCreate () { + const id = uuid.v4() + const newItem = { + id + } + onChange({ $replace: [...items, newItem] }) + } + + function handleUpdate (id, newData) { + const index = items.findIndex(item => item.id === id) + if (index === -1) { + return + } + + const newItems = [...items] + newItems[index] = newData + onChange({ $replace: newItems }) + } + + function handleDelete (id) { + const index = items.findIndex(item => item.id === id) + if (index === -1) { + return + } + + const newItems = [...items] + newItems.splice(index, 1) + onChange({ $replace: newItems }) + } + + return ( +
+ +
+ +
+
+ { + items.map(item => { + return ( +
+ handleUpdate(item.id, newData)} + onDelete={() => handleDelete(item.id)} + /> +
+ ) + }) + } +
+
+ ) +} diff --git a/app/components/PreferencesListInput/style.css b/app/components/PreferencesListInput/style.css new file mode 100644 index 00000000..79fe1699 --- /dev/null +++ b/app/components/PreferencesListInput/style.css @@ -0,0 +1,46 @@ +.PreferencesListInput-label { + display: inline-block; + margin-bottom: 10px; +} + +.PreferencesListInputItem { + margin-top: 10px; + margin-bottom: 10px; + padding: 10px 12px; + + border: 1px solid var(--base-color--shade); + border-radius: 10px; +} + +.PreferencesListInputItem > div, +.PreferencesListInputItem-flexWrapper > div { + width: 100%; +} + +.PreferencesListInputItem-flexWrapper { + display: flex; +} + +.PreferencesListInputItem-flexWrapper > div:last-child { + text-align: right; +} + +.PreferencesListInputItem-input { + margin-bottom: -10px; +} + +.PreferencesListInputItem-input--small { + width: 100px; +} + +.PreferencesListInputItem-input input { + margin-right: 5px; +} + +.PreferencesListInputItem-input select { + width: 240px; +} + +.PreferencesListInputItem-flexInputs { + display: flex; +} diff --git a/app/components/PreferencesSelectInput/index.jsx b/app/components/PreferencesSelectInput/index.jsx index 73d0bc07..bf9c5559 100644 --- a/app/components/PreferencesSelectInput/index.jsx +++ b/app/components/PreferencesSelectInput/index.jsx @@ -5,14 +5,35 @@ import * as random from '../../utils/random' export function PreferencesSelectInput ({ label, value, options = [], onChange = () => {} }) { const [id] = React.useState(`number-${random.number()}`) + return (
-
+ { + label && + <> + + + }
diff --git a/app/components/PreferencesSelectInput/style.css b/app/components/PreferencesSelectInput/style.css index a8d818c6..712aeef0 100644 --- a/app/components/PreferencesSelectInput/style.css +++ b/app/components/PreferencesSelectInput/style.css @@ -1,3 +1,4 @@ -.PreferencesSelectInput select { - margin-top: 10px; +.PreferencesSelectInput-label { + display: inline-block; + margin-bottom: 10px; } \ No newline at end of file diff --git a/app/components/PreferencesStringInput/index.jsx b/app/components/PreferencesStringInput/index.jsx index 60647452..182d7b38 100644 --- a/app/components/PreferencesStringInput/index.jsx +++ b/app/components/PreferencesStringInput/index.jsx @@ -3,12 +3,12 @@ import './style.css' import * as random from '../../utils/random' -export function PreferencesStringInput ({ label, value = '', onChange = () => {} }) { +export function PreferencesStringInput ({ label, value = '', placeholder, onChange = () => {} }) { const [id] = React.useState(`number-${random.number()}`) return (
- onChange(e.target.value)} /> + onChange(e.target.value)} />
) } diff --git a/app/components/Role/index.jsx b/app/components/Role/index.jsx index 4e492482..ea1ac369 100644 --- a/app/components/Role/index.jsx +++ b/app/components/Role/index.jsx @@ -5,6 +5,8 @@ import * as api from '../../api' import { Popover } from '../Popover' import { PopupConfirm } from '../Popup/confirm' +const MAIN_ROLE_ID = 1 + import './style.css' export function Role ({ currentRole = 0, open, onClose = () => {} }) { @@ -25,15 +27,15 @@ export function Role ({ currentRole = 0, open, onClose = () => {} }) {
Become main
This will turn the current
main client into satellite mode - +
Only the main client's selections can be triggered by the api { - currentRole === 1 + currentRole === MAIN_ROLE_ID ?
This is the main client
: ( - ) diff --git a/app/components/Role/style.css b/app/components/Role/style.css index 85b091dd..b1cb2f29 100644 --- a/app/components/Role/style.css +++ b/app/components/Role/style.css @@ -11,6 +11,10 @@ box-sizing: border-box; } +.Role-content .Button { + margin-top: 10px; +} + .Role-info { margin: 5px; } diff --git a/app/components/Sharing/index.jsx b/app/components/Sharing/index.jsx index 68205778..d27e9247 100644 --- a/app/components/Sharing/index.jsx +++ b/app/components/Sharing/index.jsx @@ -4,6 +4,7 @@ import { SharedContext } from '../../sharedContext' import { Notification } from '../Notification' import { Popover } from '../Popover' +import { Modal } from '../Modal' import CollaborationIcon from '../../assets/icons/collaboration.svg' @@ -53,24 +54,29 @@ export function Sharing ({ open, onClose = () => {} }) { }, [copied]) return ( - +
- { - HOST === 'localhost' && - ( -
- -
- ) - }
-
- Share a link to this workspace and collaborate in real time - +
- + ) } diff --git a/app/components/Sharing/style.css b/app/components/Sharing/style.css index 207e6ea8..80d74081 100644 --- a/app/components/Sharing/style.css +++ b/app/components/Sharing/style.css @@ -11,8 +11,14 @@ box-sizing: border-box; } +.Sharing-text { + padding: 0 5px; + box-sizing: border-box; +} + .Sharing-copyBtn { margin-top: 15px; + margin-bottom: 10px; } .Sharing-icon { @@ -20,5 +26,5 @@ } .Sharing-notification { - padding: 5px; + margin-top: 15px; } \ No newline at end of file diff --git a/app/components/TabsComponent/index.jsx b/app/components/TabsComponent/index.jsx index 84d5311a..abd4bafe 100644 --- a/app/components/TabsComponent/index.jsx +++ b/app/components/TabsComponent/index.jsx @@ -16,7 +16,7 @@ import { WidgetRenderer } from '../WidgetRenderer' * @param { params } param0 * @returns { React.Component } */ -export function TabsComponent ({ data, onUpdate = () => {} }) { +export function TabsComponent ({ data, widgets, onUpdate = () => {} }) { const [activeTab, setActiveTab] = React.useState() const [tabToRemove, setTabToRemove] = React.useState() @@ -169,7 +169,7 @@ export function TabsComponent ({ data, onUpdate = () => {} }) { if (!data?.children[id]) { return <> } - return handleChildUpdate(activeTab, data)} /> + return handleChildUpdate(activeTab, data)} /> } const tabs = (data?.order || []) diff --git a/app/components/VerticalNavigation/index.jsx b/app/components/VerticalNavigation/index.jsx index 78a0ee7a..b542a97a 100644 --- a/app/components/VerticalNavigation/index.jsx +++ b/app/components/VerticalNavigation/index.jsx @@ -51,7 +51,7 @@ export function VerticalNavigation ({ sections = [], active = [0, 0], onClick = { sections.map((section, i) => { return ( -
    +
      { /* Render the section's @@ -74,12 +74,12 @@ export function VerticalNavigation ({ sections = [], active = [0, 0], onClick = const isActive = activePath?.[0] === i && activePath?.[1] === j return ( handleClick(e, i, j)} > - {item.title} + {item?.title} ) }) diff --git a/app/components/WidgetRenderer/index.jsx b/app/components/WidgetRenderer/index.jsx index 3bca4b9b..3cd01d64 100644 --- a/app/components/WidgetRenderer/index.jsx +++ b/app/components/WidgetRenderer/index.jsx @@ -6,6 +6,7 @@ import { GridItem } from '../GridItem' import { TabsComponent } from '../TabsComponent' import { EmptyComponent } from '../EmptyComponent' import { FrameComponent } from '../FrameComponent' +import { MissingComponent } from '../MissingComponent' /** * Define render functions for the @@ -13,41 +14,45 @@ import { FrameComponent } from '../FrameComponent' * basic layouts */ const INTERNAL_COMPONENTS = { - 'bridge.internals.grid': (data, onUpdate) => { + 'bridge.internals.grid': (widgetId, data, onUpdate, widgets) => { return ( - + { (data.children ? Object.entries(data.children) : []) - .map(([id, component]) => ( - - onUpdate({ - children: { - [id]: data - } - })} - /> - - )) + .map(([id, component]) => { + return ( + + onUpdate({ + children: { + [id]: data + } + })} + /> + + ) + }) } ) }, - 'bridge.internals.tabs': (data, onUpdate) => { - return + 'bridge.internals.tabs': (widgetId, data, onUpdate, widgets) => { + return }, 'bridge.internals.empty': () => { return } } -export function widgetExists (component, repository = {}) { +export function widgetExists (component, widgets = {}) { if (INTERNAL_COMPONENTS[component]) { return true } - if (repository?.[component]) { + if (widgets?.[component]) { return true } @@ -63,9 +68,19 @@ export function widgetExists (component, repository = {}) { * @param { (arg1: any) => {} } onUpdate * @returns { React.ReactElement } */ -export const WidgetRenderer = ({ data, onUpdate = () => {}, forwardProps = {} }) => { - if (INTERNAL_COMPONENTS[data.component]) { - return INTERNAL_COMPONENTS[data.component](data, onUpdate) +export const WidgetRenderer = ({ widgetId, widgets, data, onUpdate = () => {}, forwardProps = {} }) => { + if (INTERNAL_COMPONENTS[data?.component]) { + return INTERNAL_COMPONENTS[data.component](widgetId, data, onUpdate, widgets) } - return + + if (!widgets || typeof widgets != 'object') { + return <> + } + + const uri = widgets?.[data?.component]?.uri + if (!uri) { + return + } + + return } diff --git a/app/index.css b/app/index.css index b5e8fa02..6e90248f 100644 --- a/app/index.css +++ b/app/index.css @@ -6,6 +6,7 @@ html, body, #root { height: 100%; min-height: 100vh; + overflow-y: hidden; } body { diff --git a/app/views/Workspace.jsx b/app/views/Workspace.jsx index 75c50407..d17a2ffa 100644 --- a/app/views/Workspace.jsx +++ b/app/views/Workspace.jsx @@ -12,12 +12,11 @@ import React from 'react' import { SharedContext } from '../sharedContext' import { Header } from '../components/Header' +import { Footer } from '../components/Footer' import { Onboarding } from '../components/Onboarding' +import { WidgetRenderer } from '../components/WidgetRenderer' import { MessageContainer } from '../components/MessageContainer' -import { MissingComponent } from '../components/MissingComponent' - -import { WidgetRenderer, widgetExists } from '../components/WidgetRenderer' /** * Get the file name without extension @@ -46,12 +45,20 @@ function getFileNameFromPath (filePath) { export const Workspace = () => { const [shared, applyShared] = React.useContext(SharedContext) + const [children, setChildren] = React.useState(shared?.children) const sharedRef = React.useRef(shared) React.useEffect(() => { sharedRef.current = shared }, [shared]) + React.useEffect(() => { + if (!shared?.children) { + return setChildren({}) + } + setChildren(shared?.children) + }, [JSON.stringify(shared.children)]) + /** * Handle updates of component data * by applying the data to the shared @@ -83,17 +90,14 @@ export const Workspace = () => { Loop through the components from the store and render them all in the interface */ - (shared.children ? Object.entries(shared.children) : []) + children && Object.entries(children) .map(([id, component]) => (
      - { - widgetExists(component.component, sharedRef.current?._widgets) - ? handleComponentUpdate({ [id]: data })} /> - : - } + handleComponentUpdate({ [id]: data })} />
      )) } +
      ) } diff --git a/app/views/WorkspaceWidget.jsx b/app/views/WorkspaceWidget.jsx index e324de35..0e776aea 100644 --- a/app/views/WorkspaceWidget.jsx +++ b/app/views/WorkspaceWidget.jsx @@ -10,6 +10,7 @@ import React from 'react' import { Header } from '../components/Header' +import { Footer } from '../components/Footer' import { SharedContext } from '../sharedContext' import { MissingComponent } from '../components/MissingComponent' @@ -77,12 +78,9 @@ export const WorkspaceWidget = () => { <>
      - { - widgetExists(widget?.component, repository) - ? handleComponentUpdate({ [id]: data })} forwardProps={{ enableFloat: false }} /> - : - } + handleComponentUpdate({ [id]: data })} forwardProps={{ enableFloat: false }} />
      +
      ) } diff --git a/docs/api/README.md b/docs/api/README.md index b6b954c4..cc6f74f9 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -97,7 +97,7 @@ Remove a listener for an event ## State The state is a shared object representing the workspace, this is used to render the UI as well as keeping track of settings -### `bridge.state.apply(set|sets)` +### `bridge.state.apply(set|sets)` `bridge.state.apply(path, set)` Apply an object to the state using a deep apply algorithm. Listeners for the event `state.change` will be immediately called. **Note** diff --git a/docs/build.md b/docs/build.md index b884442d..d946850e 100644 --- a/docs/build.md +++ b/docs/build.md @@ -6,13 +6,21 @@ Unless specified the guidelines below apply to all supported platforms. ## Creating an Electron production build -1. **Make sure a compatible version of Node is installed on your system** +### 1. Make sure a compatible version of Node is installed on your system Most often the preferred version is the latest LTS release. -2. **Install dependencies** +### 2. Install dependencies + +**Windows build requirements** +- Visual Studio with C++ desktop app build tools +- Python 3 + +**macOS build requirements** +- Xcode build tools + Run `npm ci` in the project root. This will automatically install dependencies for the core software and any bundled plugins. -3. **Create a build** +### 3. Create a build Run one of the build commands specified in `package.json`. These differ based on the platform used. A binary will be created in the `bin` directory. `npm run electron:build:mac:arm` `npm run electron:build:mac:intel` diff --git a/lib/api/SSettings.js b/lib/api/SSettings.js index 04cae782..b6c2fc06 100644 --- a/lib/api/SSettings.js +++ b/lib/api/SSettings.js @@ -10,6 +10,7 @@ */ const Validator = require('../Validator') +const uuid = require('uuid') const DIController = require('../../shared/DIController') const DIBase = require('../../shared/DIBase') @@ -22,6 +23,7 @@ class SSettings extends DIBase { #setup () { this.props.SCommands.registerAsyncCommand('settings.registerSetting', this.registerSetting.bind(this)) + this.props.SCommands.registerAsyncCommand('settings.applySetting', this.applySetting.bind(this)) } /** @@ -38,23 +40,78 @@ class SSettings extends DIBase { throw Validator.getFirstError(validate) } + /* + Add an id to the spec + for later reference + */ + const _specification = { + ...specification, + id: uuid.v4() + } + /* Make sure that the settings-group actually exists as an array in the state before pushing the specifications */ - if (!this.props.Workspace.state.data._settings?.[specification.group]) { + if (!this.props.Workspace.state.data._settings?.[_specification.group]) { + this.props.SState.applyState({ + _settings: { + [_specification.group]: [_specification] + } + }) + } else { this.props.SState.applyState({ _settings: { - [specification.group]: [] + [_specification.group]: { $push: [_specification] } } }) } + return _specification.id + } + + /** + * Apply a registered + * setting in the state + * + * @param { String } id The id of a setting to update + * @param { SettingSpecification } set A setting object to apply + * @returns { Promise. } + */ + applySetting (id, set) { + const groups = this.props.Workspace.state?.data?._settings || {} + const groupsArr = Object.entries(groups) + let groupName + let indexInGroup + + for (const [group, settings] of groupsArr) { + if (!Array.isArray(settings)) { + continue + } + + const settingIndex = settings.findIndex(setting => setting.id === id) + if (settingIndex === -1) { + continue + } + + groupName = group + indexInGroup = settingIndex + break + } + + if (!groupName || indexInGroup == null) { + return + } + + const _set = [...groups[groupName]] + _set[indexInGroup] = set + this.props.SState.applyState({ _settings: { - [specification.group]: { $push: [specification] } + [groupName]: _set } }) + return true } } diff --git a/lib/api/SState.js b/lib/api/SState.js index 2f3dda6e..30bd96d3 100644 --- a/lib/api/SState.js +++ b/lib/api/SState.js @@ -8,11 +8,6 @@ const DIController = require('../../shared/DIController') const DIBase = require('../../shared/DIBase') class SState extends DIBase { - /** - * @type { Map. } - */ - #events = new Map() - constructor (...args) { super(...args) this.#setup() diff --git a/lib/api/STime.js b/lib/api/STime.js new file mode 100644 index 00000000..8f6f3e02 --- /dev/null +++ b/lib/api/STime.js @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: 2026 Axel Boberg +// +// SPDX-License-Identifier: MIT + +const uuid = require('uuid') + +const DIController = require('../../shared/DIController') +const DIBase = require('../../shared/DIBase') + +/** + * @typedef {{ + * hours: number?, + * minutes: number?, + * seconds: number?, + * frames: number?, + * milliseconds: number? + * }} ClockFrame + */ + +const MAIN_CLOCK_INTERVAL_MS = 1000 + +class STime extends DIBase { + #mainClockIval + + constructor (...args) { + super(...args) + this.#setup() + } + + /** + * @param { Date } date + * @returns { ClockFrame } + */ + #frameFromDate (date) { + return { + hours: date.getHours(), + minutes: date.getMinutes(), + seconds: date.getSeconds() + } + } + + #setupMainClock () { + const id = this.registerClock({ + id: 'main', + label: 'Wall clock' + }) + this.#mainClockIval = setInterval(() => { + this.submitFrame(id, this.#frameFromDate(new Date())) + }, MAIN_CLOCK_INTERVAL_MS) + } + + #setup () { + this.props.SCommands.registerAsyncCommand('time.registerClock', this.registerClock.bind(this)) + this.props.SCommands.registerAsyncCommand('time.getAllClocks', this.getAllClocks.bind(this)) + this.props.SCommands.registerAsyncCommand('time.removeClock', this.removeClock.bind(this)) + this.props.SCommands.registerAsyncCommand('time.applyClock', this.applyClock.bind(this)) + this.props.SCommands.registerCommand('time.submitFrame', this.submitFrame.bind(this)) + + this.#setupMainClock() + } + + #getClockId () { + return uuid.v4() + } + + async #emitClocksChangedEvent () { + const clocks = await this.getAllClocks() + this.props.SEvents.emit('time.clocks.change', clocks) + } + + async getAllClocks () { + const clocksObj = await this.props.SState.getState('clocks') + if (!clocksObj) { + return [] + } + return Object.entries(clocksObj) + .map(([id, spec]) => { + return { + ...spec, + id + } + }) + } + + registerClock (spec) { + const id = spec?.id || this.#getClockId() + const set = { + clocks: { + [id]: { + label: spec?.label || '' + } + } + } + + this.props.SState.applyState(set) + this.#emitClocksChangedEvent() + return id + } + + removeClock (clockId) { + if (typeof clockId !== 'string') { + return + } + + const clockExists = this.props.SState.getState(`clocks.${clockId}`) + if (!clockExists) { + return + } + + this.props.SState.applyState({ + clocks: { + [clockId]: { $delete: true } + } + }) + + this.#emitClocksChangedEvent() + return true + } + + applyClock (clockId, set) { + this.props.SState.applyState({ + clocks: { + [clockId]: set + } + }) + this.#emitClocksChangedEvent() + } + + /** + * Submit a clock frame + * for a specific clock + * @param { string } clockId + * @param { ClockFrame } frame + */ + submitFrame (clockId, frame) { + this.props.SEvents.emit('time.frame', clockId, frame) + this.props.SEvents.emit(`time.frame.${clockId}`, frame) + } +} + +DIController.main.register('STime', STime, [ + 'SCommands', + 'SEvents', + 'SState' +]) diff --git a/lib/api/index.js b/lib/api/index.js index 15d16104..88e14ce0 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -5,6 +5,7 @@ const DIController = require('../../shared/DIController') require('./SAuth') +require('./STime') require('./SItems') require('./SState') require('./STypes') @@ -38,6 +39,7 @@ class SAPI { this.items = props.SItems this.state = props.SState this.types = props.STypes + this.time = props.STime this.auth = props.SAuth } } @@ -56,5 +58,6 @@ DIController.main.register('SAPI', SAPI, [ 'STypes', 'SState', 'SItems', + 'STime', 'SAuth' ]) diff --git a/lib/schemas/setting.schema.json b/lib/schemas/setting.schema.json index ba7f2ce1..3de21786 100644 --- a/lib/schemas/setting.schema.json +++ b/lib/schemas/setting.schema.json @@ -7,7 +7,7 @@ "properties": { "type": { "type": "string", - "enum": ["boolean", "theme", "number", "string", "frame", "select", "segmented"] + "enum": ["boolean", "theme", "number", "string", "button", "frame", "select", "segmented", "list"] }, "bind": { "type": "string" diff --git a/lib/schemas/shortcuts.schema.json b/lib/schemas/shortcuts.schema.json index f56a51f4..a68298c4 100644 --- a/lib/schemas/shortcuts.schema.json +++ b/lib/schemas/shortcuts.schema.json @@ -82,7 +82,9 @@ "CommandOrControl", "Backspace", "Delete", - "Space" + "Space", + ",", + "." ] } } diff --git a/lib/schemas/type.schema.json b/lib/schemas/type.schema.json index c1be87b6..27602aa5 100644 --- a/lib/schemas/type.schema.json +++ b/lib/schemas/type.schema.json @@ -22,6 +22,10 @@ "type": "string", "description": "A human readable name of the type, if omitted, this type won't be shown in the interface" }, + "ui.icon": { + "type": "string", + "description": "An icon to use for this type" + }, "category": { "type": "string", "description": "A category name for the type, for more semantic organisation" @@ -46,8 +50,7 @@ "group": { "type": "string" } - }, - "required": [ "type", "name" ] + } } } } diff --git a/lib/template.json b/lib/template.json index 94fe2515..88892e61 100644 --- a/lib/template.json +++ b/lib/template.json @@ -7,15 +7,16 @@ "title": "Rundown", "component": "bridge.internals.grid", "layout": { - "liveSwitch": { "x": 0, "y": 0, "w": 6, "h": 2 }, - "library": { "x": 0, "y": 2, "w": 6, "h": 10 }, + "time": { "x": 0, "y": 0, "w": 6, "h": 3 }, + "library": { "x": 0, "y": 3, "w": 6, "h": 9 }, "rundown": { "x": 6, "y": 0, "w": 12, "h": 12 }, "thumbnail": { "x": 18, "y": 0, "w": 6, "h": 3 }, "inspector": { "x": 18, "y": 3, "w": 6, "h": 9 } }, "children": { - "liveSwitch": { - "component": "bridge.plugins.caspar.liveSwitch" + "time": { + "component": "bridge.plugins.clock.display", + "clockId": "main" }, "library": { "component": "bridge.plugins.caspar.library" @@ -30,22 +31,6 @@ "component": "bridge.plugins.inspector" } } - }, - "2": { - "title": "Time", - "component": "bridge.internals.grid", - "layout": { - "time": { "x": 0, "y": 0, "w": 8, "h": 4 }, - "latency": { "x": 8, "y": 0, "w": 8, "h": 4 } - }, - "children": { - "time": { - "component": "bridge.plugins.clock.time" - }, - "latency": { - "component": "bridge.plugins.clock.latency" - } - } } } } diff --git a/lib/workspace/Workspace.js b/lib/workspace/Workspace.js index 68c0b8c8..a45616bf 100644 --- a/lib/workspace/Workspace.js +++ b/lib/workspace/Workspace.js @@ -84,13 +84,13 @@ class Workspace extends EventEmitter { this.#id = uuid.v4() logger.debug('Initialized workspace', this.#id) + this.state = new SavedState(initialState) + this.api = DIController.main.instantiate('SAPI', { Workspace: this }) this.sockets.setWorkspace(this) - - this.state = new SavedState(initialState) this.plugins = new PluginLoader({ paths: [paths.internalPlugins, paths.plugins], workspace: this }) /** diff --git a/media/screenshot.png b/media/screenshot.png index 7c040fcc..bfb24e6a 100644 Binary files a/media/screenshot.png and b/media/screenshot.png differ diff --git a/package-lock.json b/package-lock.json index 0df8d580..e68cde9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@dotenvx/dotenvx": "^1.38.4", "@electron/osx-sign": "^1.3.2", "@electron/packager": "^18.3.3", + "@electron/rebuild": "^4.0.2", "babel-loader": "^8.2.2", "css-loader": "^5.2.0", "electron": "^38.1.0", @@ -2214,6 +2215,34 @@ "node": ">=14.14" } }, + "node_modules/@electron/rebuild": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "got": "^11.7.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^7.5.6", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/@electron/universal": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.2.tgz", @@ -2463,6 +2492,122 @@ "dev": true, "license": "BSD-3-Clause" }, + "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, + "license": "ISC", + "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.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/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, + "license": "MIT", + "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/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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, + "license": "MIT", + "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/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3075,6 +3220,54 @@ "node": ">= 8" } }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3583,6 +3776,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3619,6 +3822,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -4163,6 +4376,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -4321,6 +4561,31 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -4370,6 +4635,84 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -4580,6 +4923,16 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -4626,6 +4979,32 @@ "node": ">= 10.0" } }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4641,6 +5020,16 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -5146,6 +5535,19 @@ "node": ">=0.10.0" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -5211,6 +5613,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5370,6 +5782,13 @@ "node": ">= 0.4" } }, + "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, + "license": "MIT" + }, "node_modules/eciesjs": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.14.tgz", @@ -5526,6 +5945,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -6338,6 +6780,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -6537,11 +6986,14 @@ } }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -6735,6 +7187,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/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, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6768,6 +7250,19 @@ "node": ">=12" } }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7340,6 +7835,20 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -7354,6 +7863,20 @@ "node": ">=10.19.0" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -7405,6 +7928,27 @@ "postcss": "^8.1.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7518,6 +8062,16 @@ "node": ">= 0.10" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -7760,6 +8314,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -7935,6 +8499,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -8107,6 +8684,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -9052,9 +9645,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -9088,6 +9681,23 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9156,6 +9766,39 @@ "semver": "bin/semver.js" } }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -9364,41 +10007,194 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, - "license": "MIT", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "minipass": "^3.0.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=8" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "yallist": "^4.0.0" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" } }, "node_modules/ms": { @@ -9460,6 +10256,29 @@ "tslib": "^2.0.3" } }, + "node_modules/node-abi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.24.0.tgz", + "integrity": "sha512-u2EC1CeNe25uVtX3EZbdQ275c74zdZmmpzrHEQh2aIYqoVjlglfUpOX9YY85x1nlBydEKDVaSmMNhR7N82Qj8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -9502,6 +10321,31 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -9513,6 +10357,22 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9585,6 +10445,22 @@ "node": ">=4" } }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -9843,6 +10719,30 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -9903,6 +10803,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -9913,6 +10826,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -10032,6 +10952,30 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -10081,9 +11025,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -10395,6 +11339,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -10719,6 +11673,19 @@ "react": ">= 16.3" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, "node_modules/read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -11171,6 +12138,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -11725,6 +12706,47 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -11807,6 +12829,19 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -11883,6 +12918,22 @@ "node": ">=8" } }, + "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -11955,6 +13006,20 @@ "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -12060,6 +13125,33 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/terser": { "version": "5.39.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", @@ -12163,6 +13255,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -12472,6 +13581,32 @@ "node": ">=4" } }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -12635,6 +13770,16 @@ "node": ">=10.13.0" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/webpack": { "version": "5.99.7", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz", @@ -12975,6 +14120,25 @@ "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, + "license": "MIT", + "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/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 60efc1c9..f69b2507 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,12 @@ "build:dev": "npm run clean | webpack --config webpack.dev.js", "clean": "node ./scripts/clean-build-folder.js", "electron": "NODE_ENV=development electron --trace-warnings index.js", - "electron:build:mac:arm": "npm run build && electron-packager . \"Bridge\" --platform=darwin --arch=arm64 --extend-info extra.plist --icon=media/appicon.icns --overwrite --asar --ignore=\"webpack.*\\.js\" --out ./bin", - "electron:build:mac:intel": "npm run build && electron-packager . \"Bridge\" --platform=darwin --arch=x64 --extend-info extra.plist --icon=media/appicon.icns --overwrite --asar --ignore=\"webpack.*\\.js\" --out ./bin", - "electron:build:win": "npm run build && electron-packager . \"Bridge\" --platform=win32 --arch=x64 --extend-info extra.plist --icon=media/appicon.ico --overwrite --asar --ignore=\"webpack.*\\.js\" --out ./bin", + "electron:rebuild": "electron-rebuild -f", + "electron:prebuild:arm64": "node scripts/electron-prebuild arm64", + "electron:prebuild:x64": "node scripts/electron-prebuild x64", + "electron:build:mac:arm": "npm run build && npm run electron:prebuild:arm64 && electron-packager . \"Bridge\" --platform=darwin --arch=arm64 --extend-info extra.plist --icon=media/appicon.icns --overwrite --ignore=\"webpack.*\\.js\" --ignore=\"docs/*\" --ignore=\"./README.md\" --ignore=\"./Dockerfile\" --ignore=\"./docker-compose.yml\" --out ./bin && node scripts/electron-postbuild-macos.js bin/Bridge-darwin-arm64/Bridge.app", + "electron:build:mac:intel": "npm run build && npm run electron:prebuild:x64 && electron-packager . \"Bridge\" --platform=darwin --arch=x64 --extend-info extra.plist --icon=media/appicon.icns --overwrite --asar --ignore=\"webpack.*\\.js\" --out ./bin && node scripts/electron-postbuild-macos.js bin/Bridge-darwin-x64/Bridge.app", + "electron:build:win": "npm run build && electron-packager . \"Bridge\" --platform=win32 --arch=x64 --extend-info extra.plist --icon=media/appicon.ico --overwrite --ignore=\"webpack.*\\.js\" --ignore=\"docs/*\" --ignore=\"./README.md\" --ignore=\"./Dockerfile\" --ignore=\"./docker-compose.yml\" --out ./bin", "electron:sign:mac": "node scripts/sign-macos.js", "prepare": "husky install", "postinstall": "node scripts/install-plugin-dependencies.js" @@ -57,6 +60,7 @@ "@dotenvx/dotenvx": "^1.38.4", "@electron/osx-sign": "^1.3.2", "@electron/packager": "^18.3.3", + "@electron/rebuild": "^4.0.2", "babel-loader": "^8.2.2", "css-loader": "^5.2.0", "electron": "^38.1.0", @@ -77,5 +81,13 @@ "webpack-asset-map": "^1.0.1", "webpack-cli": "^4.5.0", "webpack-merge": "^5.7.3" - } + }, + "build": { + "mac": { + "extendInfo": { + "NSMicrophoneUsageDescription": "This app requires access to the microphone to decode SMPTE timecode." + } + } + }, + "nodeGypRebuild": "true" } diff --git a/plugins/caspar/lib/types.js b/plugins/caspar/lib/types.js index ce64deba..6b47dbc0 100644 --- a/plugins/caspar/lib/types.js +++ b/plugins/caspar/lib/types.js @@ -19,6 +19,9 @@ function init (htmlPath) { category: 'Caspar', inherits: 'bridge.types.playable', properties: { + name: { + default: 'AMCP' + }, 'caspar.server': { name: 'Server', type: 'string', @@ -146,6 +149,9 @@ function init (htmlPath) { category: 'Caspar', inherits: 'bridge.caspar.media', properties: { + name: { + default: 'Load' + }, 'caspar.auto': { name: 'Auto play', type: 'boolean', @@ -161,15 +167,22 @@ function init (htmlPath) { category: 'Caspar', inherits: 'bridge.caspar.playable', properties: { + name: { + default: 'Template: $(this.data.caspar.data.f0)' + }, 'caspar.target': { name: 'Target', type: 'string', allowsVariables: true, 'ui.group': 'Caspar' }, + 'caspar.data': { + default: { f0: 'Foo' } + }, 'caspar.templateDataSource': { name: 'Data', type: 'string', + default: '{\n "f0": "Foo"\n}', allowsVariables: true, 'ui.group': 'Caspar', 'ui.uri': `${htmlPath}?path=inspector/template` @@ -181,7 +194,12 @@ function init (htmlPath) { id: 'bridge.caspar.template.update', name: 'Template update', category: 'Caspar', - inherits: 'bridge.caspar.template' + inherits: 'bridge.caspar.template', + properties: { + name: { + default: 'Template update: $(this.data.caspar.data.f0)' + } + } }) bridge.types.registerType({ @@ -189,7 +207,11 @@ function init (htmlPath) { name: 'Clear', category: 'Caspar', inherits: 'bridge.caspar.playable', - properties: {} + properties: { + name: { + default: 'Clear' + } + } }) bridge.types.registerType({ @@ -198,6 +220,9 @@ function init (htmlPath) { category: 'Caspar', inherits: 'bridge.caspar.mixable', properties: { + name: { + default: 'Opacity' + }, 'caspar.opacity': { name: 'Opacity', type: 'string', @@ -213,6 +238,9 @@ function init (htmlPath) { category: 'Caspar', inherits: 'bridge.caspar.mixable', properties: { + name: { + default: 'Transform' + }, 'caspar.x': { name: 'X', type: 'string', @@ -250,6 +278,9 @@ function init (htmlPath) { category: 'Caspar', inherits: 'bridge.caspar.mixable', properties: { + name: { + default: 'Volume' + }, 'caspar.volume': { name: 'Volume', type: 'string', diff --git a/plugins/clock/app/App.jsx b/plugins/clock/app/App.jsx index dbe94cde..1bc5b6a4 100644 --- a/plugins/clock/app/App.jsx +++ b/plugins/clock/app/App.jsx @@ -1,68 +1,6 @@ import React from 'react' -import bridge from 'bridge' - -import Average from './Average' -import { CurrentTime } from './components/CurrentTime' - -/** - * Declare how large our running - * average should be timewise - * @type { Number } - */ -const AVERAGE_TIME_SPAN_MS = 20000 - -/** - * Declare how often we should request a - * new timestamp from the main process - * - * Add a random part as we don't want - * all clocks to sync at the same time - * - * @type { Number } - */ -const CHECK_INTERVAL_MS = 9000 + Math.random() * 1000 +import { SelectableClock } from './views/SelectableClock' export default function App () { - const [latency, setLatency] = React.useState(0) - const [time, setTime] = React.useState(Date.now()) - - const [view, setView] = React.useState() - - React.useEffect(() => { - const average = new Average(AVERAGE_TIME_SPAN_MS) - - /* - Fetch a new timestamp from - the main process and measure - the roundtrip time to keep a - running average - */ - setInterval(async () => { - const { echo, time } = await bridge.commands.executeCommand('bridge.plugins.clock.time', Date.now()) - const latency = (Date.now() - echo) / 2 - - average.add(latency) - - setLatency(average.read()) - setTime(time) - }, CHECK_INTERVAL_MS) - }, []) - - /* - - */ - React.useEffect(() => { - const params = new URLSearchParams(window.location.search) - setView(params.get('view')) - }, []) - - return ( -
      - { - view === 'latency' - ? {Math.floor(latency * 10) / 10}ms - : - } -
      - ) + return } diff --git a/plugins/clock/app/components/Clock/index.jsx b/plugins/clock/app/components/Clock/index.jsx new file mode 100644 index 00000000..865dba08 --- /dev/null +++ b/plugins/clock/app/components/Clock/index.jsx @@ -0,0 +1,39 @@ +import React from 'react' +import './style.css' + +function zeroPad (num) { + if (typeof num !== 'number') return num + if (num < 10) return `0${num}` + return `${num}` +} + +export const Clock = ({ frame }) => { + const components = React.useMemo(() => { + if (!frame) { + return ['--', '--', '--'] + } + + return [ + frame.hours, + frame.minutes, + frame.seconds, + frame.frames, + frame.milliseconds + ] + .filter(component => component != null) + }, [frame]) + + return ( +
      +
      + { + (components || []) + .map(component => { + return zeroPad(component) + }) + .join(':') + } +
      +
      + ) +} diff --git a/plugins/clock/app/components/Clock/style.css b/plugins/clock/app/components/Clock/style.css new file mode 100644 index 00000000..c0cd5670 --- /dev/null +++ b/plugins/clock/app/components/Clock/style.css @@ -0,0 +1,15 @@ +.Clock { + display: flex; + width: 100%; + height: 100%; + + font-variant-numeric: tabular-nums; + font-size: min(7vw, 20vh); + + align-items: center; + justify-content: center; +} + +.Clock-components { + font-size: 2em; +} diff --git a/plugins/clock/app/components/CurrentTime/index.jsx b/plugins/clock/app/components/CurrentTime/index.jsx deleted file mode 100644 index 1c1cd672..00000000 --- a/plugins/clock/app/components/CurrentTime/index.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import './style.css' - -function zeroPad (num) { - if (num < 10) return `0${num}` - return `${num}` -} - -/** - * Wrap every character - * of a string in a span - * @param { String } str - * @returns { React.ReactElement[] } - */ -function spanWrapCharacters (str = '') { - return String(str) - .split('') - .map((char, i) => {char}) -} - -export const CurrentTime = ({ className = '', offset = 0, base = Date.now() }) => { - const [milliseconds, setMilliseconds] = React.useState(0) - - React.useEffect(() => { - const start = Date.now() - const ival = window.setInterval(() => { - setMilliseconds(Date.now() - start) - }, 50) - - setMilliseconds(0) - return () => { - clearInterval(ival) - } - }, [base]) - - const time = base + offset + milliseconds - const date = new Date(time) - - const hours = zeroPad(date.getHours()) - const minutes = zeroPad(date.getMinutes()) - const seconds = zeroPad(date.getSeconds()) - const hundreds = Math.floor(date.getMilliseconds() / 100) - - return ( - - {spanWrapCharacters(hours)}:{spanWrapCharacters(minutes)}:{spanWrapCharacters(seconds)}.{spanWrapCharacters(hundreds)} - - ) -} diff --git a/plugins/clock/app/components/CurrentTime/style.css b/plugins/clock/app/components/CurrentTime/style.css deleted file mode 100644 index 6e872d66..00000000 --- a/plugins/clock/app/components/CurrentTime/style.css +++ /dev/null @@ -1,10 +0,0 @@ - - -.CurrentTime-char { - display: inline-block; - width: 0.6em; -} - -.CurrentTime-faded { - opacity: 0.4; -} \ No newline at end of file diff --git a/plugins/clock/app/components/SelectableClock/index.jsx b/plugins/clock/app/components/SelectableClock/index.jsx new file mode 100644 index 00000000..b6f802ea --- /dev/null +++ b/plugins/clock/app/components/SelectableClock/index.jsx @@ -0,0 +1,80 @@ +import React from 'react' +import bridge from 'bridge' + +import './style.css' + +import { Clock } from '../Clock' + +const DEFAULT_CLOCK_ID = 'main' + +export const SelectableClock = ({ clockId: _clockId = window.WIDGET_DATA?.['clockId'] || DEFAULT_CLOCK_ID }) => { + const [clocks, setClocks] = React.useState([]) + const [clockId, setClockId] = React.useState(_clockId) + const [lastFrame, setLastFrame] = React.useState() + + React.useEffect(() => { + function onClocksChange (newClocks) { + setClocks(newClocks) + } + + bridge.events.on('time.clocks.change', onClocksChange) + return () => { + bridge.events.off('time.clocks.change', onClocksChange) + } + }, []) + + React.useEffect(() => { + async function initiallyUpdateClocks () { + const clocks = await bridge.time.getAllClocks() + setClocks(clocks) + } + initiallyUpdateClocks() + }, []) + + React.useEffect(() => { + if (!clockId) { + return + } + + function onFrame (newFrame) { + setLastFrame(newFrame) + } + + function unload () { + bridge.events.off(`time.frame.${clockId}`, onFrame) + } + + setLastFrame(undefined) + bridge.events.on(`time.frame.${clockId}`, onFrame) + window.addEventListener('beforeunload', unload) + return () => { + window.removeEventListener('beforeunload', unload) + unload() + } + }, [clockId]) + + function handleClockSelectChange (e) { + setClockId(e.target.value) + window.WIDGET_UPDATE({ + 'clockId': e.target.value + }) + } + + return ( +
      +
      + +
      +
      + +
      +
      + ) +} diff --git a/plugins/clock/app/components/SelectableClock/style.css b/plugins/clock/app/components/SelectableClock/style.css new file mode 100644 index 00000000..fdd30c6c --- /dev/null +++ b/plugins/clock/app/components/SelectableClock/style.css @@ -0,0 +1,20 @@ +.SelectableClock { + display: flex; + width: 100%; + height: 100%; + + flex-direction: column; +} + +.SelectableClock-header { + padding: 0 5px 0 5px; + box-sizing: border-box; +} + +.SelectableClock-header select { + width: 100%; +} + +.SelectableClock-body { + height: 100%; +} \ No newline at end of file diff --git a/plugins/clock/app/style.css b/plugins/clock/app/style.css index 82d9fc6a..4eb432d9 100644 --- a/plugins/clock/app/style.css +++ b/plugins/clock/app/style.css @@ -7,8 +7,6 @@ html, body, #root { padding: 0; margin: 0; - - font-size: min(8vw, 30vh); } .Clock-wrapper { @@ -19,7 +17,3 @@ html, body, #root { align-items: center; justify-content: center; } - -.Clock-digits { - font-size: 2em; -} diff --git a/plugins/clock/app/views/SelectableClock.jsx b/plugins/clock/app/views/SelectableClock.jsx new file mode 100644 index 00000000..ba33d779 --- /dev/null +++ b/plugins/clock/app/views/SelectableClock.jsx @@ -0,0 +1,8 @@ +import React from 'react' +import { SelectableClock as SelectableClockComponent } from '../components/SelectableClock' + +export function SelectableClock () { + return ( + + ) +} \ No newline at end of file diff --git a/plugins/clock/index.js b/plugins/clock/index.js index 055dc8c1..0340ce1a 100644 --- a/plugins/clock/index.js +++ b/plugins/clock/index.js @@ -33,18 +33,10 @@ async function initWidget () { const htmlPath = await bridge.server.serveString(html) bridge.widgets.registerWidget({ - id: 'bridge.plugins.clock.latency', - name: 'Command latency', - uri: `${htmlPath}?view=latency`, - description: 'A widget showing the latency between commands sent to the main thread', - supportsFloat: true - }) - - bridge.widgets.registerWidget({ - id: 'bridge.plugins.clock.time', - name: 'Current time', - uri: `${htmlPath}?view=time`, - description: 'A widget showing the current time', + id: 'bridge.plugins.clock.display', + name: 'Time display', + uri: `${htmlPath}`, + description: 'Use this widget to display time from a system clock or timecode input', supportsFloat: true }) } diff --git a/plugins/http/package.json b/plugins/http/package.json index 440f2cdd..75cbbb5a 100644 --- a/plugins/http/package.json +++ b/plugins/http/package.json @@ -33,7 +33,11 @@ "name": "GET", "category": "HTTP", "inherits": "bridge.http.request", - "properties": {} + "properties": { + "name": { + "default": "GET: $(this.data.http.url)" + } + } } ] }, diff --git a/plugins/inspector/app/components/Form/index.jsx b/plugins/inspector/app/components/Form/index.jsx index ba01577a..564c38c0 100644 --- a/plugins/inspector/app/components/Form/index.jsx +++ b/plugins/inspector/app/components/Form/index.jsx @@ -169,31 +169,45 @@ export function Form () { */ const variableContext = {this: firstItem, ...globalVariableContext} + async function conditionallyUpdateGroups () { + const types = store.items.map(item => item.type) + + const typeObjectPromises = removeDuplicates(types) + .map(type => bridge.types.getType(type)) + const typeObjects = await Promise.all(typeObjectPromises) + + const properties = getCommonProperties(typeObjects) + const evaluated = await bridge.state.evaluate(properties) + + const newGroups = orderByGroups(evaluated) + + if (JSON.stringify(newGroups) !== JSON.stringify(groups)) { + setGroups(newGroups) + } + } + /* Find out what the common properties are to sort them into groups and render the inputs */ React.useEffect(() => { - async function getTypes () { - const types = store.items.map(item => item.type) - const typesPromises = removeDuplicates(types) - .map(type => bridge.types.getType(type)) - - const typeObjects = await Promise.all(typesPromises) - const properties = getCommonProperties(typeObjects) - const groups = orderByGroups(properties) + async function updateGroups () { + await conditionallyUpdateGroups() /* Reset the local data as the selection changes */ setLocalData({}) - setGroups(groups) } - getTypes() + updateGroups() }, [store.selection]) + React.useEffect(() => { + conditionallyUpdateGroups() + }, [shared]) + /** * Update the state with new * values for all selected items diff --git a/plugins/inspector/app/components/SelectInput/index.jsx b/plugins/inspector/app/components/SelectInput/index.jsx index eabd0b06..46d52b17 100644 --- a/plugins/inspector/app/components/SelectInput/index.jsx +++ b/plugins/inspector/app/components/SelectInput/index.jsx @@ -18,12 +18,25 @@ export function SelectInput ({ value={value} onChange={e => onChange(e.target.value)} > - { - (data?.enum || []) - .map((value, i) => { - return - }) - } + { + (data?.enum || []) + .map((value, i) => { + let label = value + let key = i + + /* + Allow for using a custom id + if the option is an object + containing the keys 'id' and 'label' + */ + if (typeof value === 'object' && value.hasOwnProperty('value')) { + label = value.label + key = value.value + } + + return + }) + } ) } diff --git a/plugins/osc/lib/types.js b/plugins/osc/lib/types.js index 2fc75574..c530ca31 100644 --- a/plugins/osc/lib/types.js +++ b/plugins/osc/lib/types.js @@ -7,10 +7,13 @@ const bridge = require('bridge') function init (htmlPath) { bridge.types.registerType({ id: 'bridge.osc.trigger', - name: 'Trigger', + name: 'Send', category: 'OSC', inherits: 'bridge.types.delayable', properties: { + name: { + default: 'Send OSC' + }, 'osc.target': { name: 'Target', type: 'string', @@ -48,6 +51,9 @@ function init (htmlPath) { category: 'OSC', inherits: 'bridge.types.delayable', properties: { + name: { + default: 'OSC: Activate UDP' + }, 'osc.active': { name: 'Activate server', type: 'boolean', @@ -62,6 +68,9 @@ function init (htmlPath) { category: 'OSC', inherits: 'bridge.types.delayable', properties: { + name: { + default: 'OSC: Activate TCP' + }, 'osc.active': { name: 'Activate server', type: 'boolean', diff --git a/plugins/rundown/app/App.jsx b/plugins/rundown/app/App.jsx index a81ade16..f2226617 100644 --- a/plugins/rundown/app/App.jsx +++ b/plugins/rundown/app/App.jsx @@ -1,5 +1,4 @@ import React from 'react' -import bridge from 'bridge' import { SharedContextProvider } from './sharedContext' diff --git a/plugins/rundown/app/components/Header/index.jsx b/plugins/rundown/app/components/Header/index.jsx index 48df5b79..e06175ec 100644 --- a/plugins/rundown/app/components/Header/index.jsx +++ b/plugins/rundown/app/components/Header/index.jsx @@ -83,7 +83,7 @@ export function Header () {
- handleLoadMainRundown()}>Main rundown + handleLoadMainRundown()}>Main rundown { rundownInfo?.name && / {rundownInfo?.name} diff --git a/plugins/rundown/app/components/Header/style.css b/plugins/rundown/app/components/Header/style.css index bdc73764..acef4117 100644 --- a/plugins/rundown/app/components/Header/style.css +++ b/plugins/rundown/app/components/Header/style.css @@ -37,7 +37,7 @@ cursor: pointer; } -.Header-pathPart:not(:last-child):active::before { +.Header-pathPart::before { content: ''; position: absolute; @@ -46,10 +46,17 @@ width: calc(100% + 14px); height: calc(100% + 10px); - background: var(--base-color--shade); border-radius: 6px; } +.Header-pathPart.is-clickable:hover::before { + background: var(--base-color--shade); +} + +.Header-pathPart:not(:last-child):active::before { + opacity: 0.7; +} + .Header-pathPart:last-child { pointer-events: none; opacity: 0.5; diff --git a/plugins/rundown/app/components/RundownGroupItem/index.jsx b/plugins/rundown/app/components/RundownGroupItem/index.jsx index 55d5c02a..ffade126 100644 --- a/plugins/rundown/app/components/RundownGroupItem/index.jsx +++ b/plugins/rundown/app/components/RundownGroupItem/index.jsx @@ -11,8 +11,6 @@ import { RundownItemProgress } from '../RundownItemProgress' import { RundownList } from '../RundownList' import { Icon } from '../Icon' -import { ContextMenuItem } from '../../../../../app/components/ContextMenuItem' - export function RundownGroupItem ({ index, item }) { const [shared] = React.useContext(SharedContext) @@ -172,11 +170,9 @@ export function RundownGroupItem ({ index, item }) { ) } -export function getContextMenuItems (item) { +export function getContextMenuItems (ctx, item) { function handleEnterGroup () { - window.WIDGET_UPDATE({ - 'rundown.id': item.id - }) + ctx.setRundownId(item.id) } return [ diff --git a/plugins/rundown/app/components/RundownGroupItem/style.css b/plugins/rundown/app/components/RundownGroupItem/style.css index 546aef8b..52411f0a 100644 --- a/plugins/rundown/app/components/RundownGroupItem/style.css +++ b/plugins/rundown/app/components/RundownGroupItem/style.css @@ -14,7 +14,7 @@ display: flex; width: 100%; - padding: 1em; + padding: 0.9em 1em; box-sizing: border-box; } diff --git a/plugins/rundown/app/components/RundownItem/index.jsx b/plugins/rundown/app/components/RundownItem/index.jsx index 976f724d..002cc935 100644 --- a/plugins/rundown/app/components/RundownItem/index.jsx +++ b/plugins/rundown/app/components/RundownItem/index.jsx @@ -16,6 +16,7 @@ import './style.css' import { SharedContext } from '../../sharedContext' import { useAsyncValue } from '../../hooks/useAsyncValue' +import { Icon as MainAppIcon } from '../../../../../app/components/Icon' import { RundownItemProgress } from '../RundownItemProgress' import { RundownItemTimeSection } from '../RundownItemTimeSection' import { RundownItemIndicatorsSection } from '../RundownItemIndicatorsSection' @@ -77,7 +78,7 @@ async function getReadablePropertiesForType (typeName) { } } -export function RundownItem ({ index, item }) { +export function RundownItem ({ index, icon, item }) { const [shared] = React.useContext(SharedContext) const [typeProperties, setTypeProperties] = React.useState([]) @@ -128,6 +129,14 @@ export function RundownItem ({ index, item }) {
{index}
+ { + icon && + ( +
+ +
+ ) + }
{name}
diff --git a/plugins/rundown/app/components/RundownItem/style.css b/plugins/rundown/app/components/RundownItem/style.css index f3588096..9d0e0ca4 100644 --- a/plugins/rundown/app/components/RundownItem/style.css +++ b/plugins/rundown/app/components/RundownItem/style.css @@ -11,7 +11,7 @@ } .RundownItem-margin { - margin: 1em; + margin: 0.9em 1em; width: 100%; } @@ -52,6 +52,14 @@ opacity: 0.5; } +.RundownItem-icon { + display: flex; + height: 0px; + margin-right: 8px; + margin-left: -10px; + align-items: center; +} + .RundownItem-name { margin-right: 10px; } diff --git a/plugins/rundown/app/components/RundownList/index.jsx b/plugins/rundown/app/components/RundownList/index.jsx index 28535c22..60ba22e1 100644 --- a/plugins/rundown/app/components/RundownList/index.jsx +++ b/plugins/rundown/app/components/RundownList/index.jsx @@ -6,6 +6,7 @@ import './style.css' import { SharedContext } from '../../sharedContext' import { RundownVariableItem } from '../RundownVariableItem' +import { RundownTriggerItem } from '../RundownTriggerItem' import { RundownDividerItem } from '../RundownDividerItem' import { RundownGroupItem, getContextMenuItems as rundownGroupItemGetContextMenuItems } from '../RundownGroupItem' import { RundownListItem } from '../RundownListItem' @@ -23,10 +24,11 @@ import * as keyboard from '../../utils/keyboard' */ const TYPE_COMPONENTS = { 'bridge.variables.variable': { item: RundownVariableItem }, + 'bridge.types.trigger': { item: RundownTriggerItem }, 'bridge.types.divider': { item: RundownDividerItem }, 'bridge.types.group': { item: RundownGroupItem, - getContextMenuItems: item => rundownGroupItemGetContextMenuItems(item) + getContextMenuItems: (ctx, item) => rundownGroupItemGetContextMenuItems(ctx, item) } } @@ -81,11 +83,13 @@ export function RundownList ({ rundownId = '', className = '', indexPrefix = '', - disableShortcuts = false + disableShortcuts = false, + onChangeRundownId = () => {} }) { const [shared] = React.useContext(SharedContext) const elRef = React.useRef() + const typeCacheRef = React.useRef({}) const itemIds = shared?.items?.[rundownId]?.children || [] @@ -95,6 +99,14 @@ export function RundownList ({ return elRef.current.querySelector(`[data-item-id="${id}"]`) } + /* + Clear the type cache whenever + registered types change + */ + React.useEffect(() => { + typeCacheRef.current = {} + }, [shared?._types]) + /** * Focus a list item based on the * item's id that it's rendering @@ -369,6 +381,42 @@ export function RundownList ({ } } + function getCachedType (typeId) { + return typeCacheRef?.current?.[typeId] + } + + function setCachedType (typeId, typeData) { + if (!typeCacheRef.current) { + typeCacheRef.current = {} + } + typeCacheRef.current[typeId] = typeData + } + + function getRenderedType (typeId) { + const cachedType = getCachedType(typeId) + if (cachedType) { + return cachedType + } + + const rendered = bridge.types.renderType(typeId, shared?._types || {}) + setCachedType(typeId, rendered) + return rendered + } + + /* + Find a matching component based + on the ancestors of the type + */ + function getTypeComponent (typeId) { + const type = getRenderedType(typeId) + for (const ancestor of [...type.ancestors, typeId].reverse()) { + if (TYPE_COMPONENTS[ancestor]?.item) { + return TYPE_COMPONENTS[ancestor]?.item + } + } + return RundownItem + } + return (
bridge.items.getLocalItem(id)) .filter(item => item) .map((item, i) => { + const ItemComponent = getTypeComponent(item.type) const isSelected = bridge.client.selection.isSelected(item.id) - const ItemComponent = TYPE_COMPONENTS[item.type]?.item || RundownItem let contextMenuItems if (typeof TYPE_COMPONENTS[item.type]?.getContextMenuItems === 'function') { - contextMenuItems = TYPE_COMPONENTS[item.type].getContextMenuItems(item) + contextMenuItems = TYPE_COMPONENTS[item.type].getContextMenuItems({ + setRundownId: onChangeRundownId + }, item) } return ( diff --git a/plugins/rundown/app/components/RundownTriggerItem/index.jsx b/plugins/rundown/app/components/RundownTriggerItem/index.jsx new file mode 100644 index 00000000..561b57a2 --- /dev/null +++ b/plugins/rundown/app/components/RundownTriggerItem/index.jsx @@ -0,0 +1,8 @@ +import React from 'react' +import './style.css' + +import { RundownItem } from '../RundownItem' + +export function RundownTriggerItem ({ index, item }) { + return +} diff --git a/plugins/rundown/app/components/RundownTriggerItem/style.css b/plugins/rundown/app/components/RundownTriggerItem/style.css new file mode 100644 index 00000000..e69de29b diff --git a/plugins/rundown/app/components/RundownVariableItem/index.jsx b/plugins/rundown/app/components/RundownVariableItem/index.jsx index 751ab920..33cf3569 100644 --- a/plugins/rundown/app/components/RundownVariableItem/index.jsx +++ b/plugins/rundown/app/components/RundownVariableItem/index.jsx @@ -1,29 +1,8 @@ import React from 'react' import './style.css' -import * as Layout from '../Layout' +import { RundownItem } from '../RundownItem' export function RundownVariableItem ({ index, item }) { - return ( -
- -
-
- {index} -
-
- {item?.data?.name} -
-
- {item?.data?.notes} -
-
-
-
- {item?.data?.variable?.key} = {item?.data?.variable?.value} -
-
-
-
- ) + return } diff --git a/plugins/rundown/app/components/RundownVariableItem/style.css b/plugins/rundown/app/components/RundownVariableItem/style.css index b81c8bc0..e69de29b 100644 --- a/plugins/rundown/app/components/RundownVariableItem/style.css +++ b/plugins/rundown/app/components/RundownVariableItem/style.css @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 Sveriges Television AB - * - * SPDX-License-Identifier: MIT - */ - -.RundownVariableItem { - position: relative; - display: flex; - - width: 100%; - padding: 1em; - - background-color: var(--base-color-type--variable); -} - -.RundownVariableItem-index { - margin-right: 1em; - opacity: 0.5; -} - -.RundownVariableItem-property { - margin-right: 10px; -} - -.RundownVariableItem-notes { - opacity: 0.6; -} - -.RundownVariableItem-section { - display: flex; - align-items: center; -} - -.RundownVariableItem-variable { - opacity: 0.6; -} diff --git a/plugins/rundown/app/utils/contextMenu.js b/plugins/rundown/app/utils/contextMenu.js index f5422431..86fae380 100644 --- a/plugins/rundown/app/utils/contextMenu.js +++ b/plugins/rundown/app/utils/contextMenu.js @@ -1,5 +1,16 @@ +const bridge = require('bridge') + const NO_CATEGORY_ID = '__none__' +function renderAllTypes (types) { + const out = {} + Object.entries(types) + .forEach(([id]) => { + out[id] = bridge.types.renderType(id, types) + }) + return out +} + function orderTypesByCategory (types) { const out = {} @@ -19,7 +30,8 @@ function orderTypesByCategory (types) { } export function generateAddContextMenuItems (types, onItemClick) { - const categories = orderTypesByCategory(types) + const renderedTypes = renderAllTypes(types) + const categories = orderTypesByCategory(renderedTypes) return Object.entries(categories) /* diff --git a/plugins/rundown/app/views/Rundown.jsx b/plugins/rundown/app/views/Rundown.jsx index 3290d281..e29fc022 100644 --- a/plugins/rundown/app/views/Rundown.jsx +++ b/plugins/rundown/app/views/Rundown.jsx @@ -4,16 +4,23 @@ import bridge from 'bridge' import { SharedContext } from '../sharedContext' import { RundownList } from '../components/RundownList' -import * as config from '../config' import * as contextMenu from '../utils/contextMenu' +import * as config from '../config' + +const INITIAL_RUNDOWN_ID = window.WIDGET_DATA?.['rundown.id'] || config.DEFAULT_RUNDOWN_ID export function Rundown () { + const [rundownId, setRundownId] = React.useState(INITIAL_RUNDOWN_ID) const [shared] = React.useContext(SharedContext) - const [contextPos, setContextPos] = React.useState() const elRef = React.useRef() - const rundownId = window.WIDGET_DATA?.['rundown.id'] || config.DEFAULT_RUNDOWN_ID + function handleNewRundownId (newId) { + window.WIDGET_UPDATE({ + 'rundown.id': newId + }) + setRundownId(newId) + } async function handleItemCreate (typeId) { const itemId = await bridge.items.createItem(typeId) @@ -144,7 +151,10 @@ export function Rundown () { return (
handleContextMenu(e)}> - + handleNewRundownId(newId)} + />
) } diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 22a54a5c..a1801339 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -55,6 +55,12 @@ "action": "openPalette", "description": "Open the palette", "trigger": ["CommandOrControl", "P"] + }, + { + "id": "openSettings", + "action": "openSettings", + "description": "Open settings", + "trigger": ["CommandOrControl", ","] } ] }, diff --git a/plugins/timecode/README.md b/plugins/timecode/README.md new file mode 100644 index 00000000..e8eebe75 --- /dev/null +++ b/plugins/timecode/README.md @@ -0,0 +1,4 @@ +# Timecode plugin +Bridge's default timecode plugin + +## Table of contents diff --git a/plugins/timecode/index.js b/plugins/timecode/index.js new file mode 100644 index 00000000..158754c0 --- /dev/null +++ b/plugins/timecode/index.js @@ -0,0 +1,428 @@ +// SPDX-FileCopyrightText: 2026 Axel Boberg +// +// SPDX-License-Identifier: MIT + +/** + * @type { import('../../api').Api } + */ +const bridge = require('bridge') + +const manifest = require('./package.json') +const audio = require('./lib/audio') + +const DIController = require('./lib/DIController') + +const LTCDecoder = require('./lib/ltc/LTCDecoder') +// eslint-disable-next-line +const LTCDevice = require('./lib/ltc/LTCDevice') + +const TimecodeFrame = require('./lib/TimecodeFrame') + +const Logger = require('../../lib/Logger') +const logger = new Logger({ name: 'TimecodePlugin' }) + +const Cache = require('./lib/Cache') +const cache = new Cache() + +const NO_AUDIO_DEVICE_ID = 'none' +const TIMECODE_TRIGGER_TYPE = 'bridge.timecode.trigger' + +const TRIGGER_CUES_CACHE_TTL_MS = 100 + +/** + * Keep an index of all currently + * running LTC devices + * @type { Object. } + */ +const LTC_DEVICES = {} + +async function makeInputSetting (inputs = [], replaceInputs) { + const noInput = { + id: NO_AUDIO_DEVICE_ID, + label: 'None' + } + + let _inputs = [noInput, ...inputs] + if (replaceInputs) { + _inputs = { + $replace: _inputs + } + } + + return { + group: 'Timecode', + title: 'LTC devices', + inputs: [ + { + type: 'list', + label: 'Manage LTC devices used for parsing incoming SMPTE timecode', + bind: 'shared.plugins.bridge-plugin-timecode.settings.ltc_devices', + settings: [ + { + title: 'Name', + inputs: [ + { + type: 'string', + bind: 'name', + placeholder: 'Name' + } + ] + }, + { + title: 'Audio device', + inputs: [ + { + type: 'select', + bind: 'deviceId', + options: _inputs + } + ] + }, + { + title: 'Frame rate', + inputs: [ + { + type: 'segmented', + bind: 'frameRateIndex', + segments: LTCDecoder.SUPPORTED_FRAME_RATES + } + ] + } + ] + } + ] + } +} + +function makeRescanSetting (isLoading) { + return { + group: 'Timecode', + title: 'Audio devices', + inputs: [ + { + type: 'button', + label: 'Rescan for audio devices connected to the Bridge host', + buttonText: 'Rescan', + buttonIsLoading: isLoading, + command: 'timecode.rescanAudioDevices' + } + ] + } +} + +async function registerClockForInput (inputId, label) { + if (typeof inputId !== 'string') { + throw new Error('Missing or invalid input id, must be a string') + } + + const clockAlreadyExistsForInput = await getClockIdForInput(inputId) + if (clockAlreadyExistsForInput) { + return + } + + const clockId = await bridge.time.registerClock({ + label + }) + + bridge.state.apply(`plugins.${manifest.name}.clocks`, { + [inputId]: clockId + }) + + return clockId +} + +function getClockIdForInput (inputId) { + return bridge.state.get(`plugins.${manifest.name}.clocks.${inputId}`) +} + +function getClockIdForInputLocal (inputId) { + const localState = bridge.state.getLocalState() + return localState?.plugins?.[manifest.name]?.clocks?.[inputId] +} + +async function removeClock (inputId, clockId) { + /* + Remove the reference to the + clock stored by this plugin + */ + if (inputId) { + await bridge.state.apply(`plugins.${manifest.name}.clocks`, { + [inputId]: { $delete: true } + }) + } + + /* + Remove the actual clock + from the time api + */ + if (clockId) { + await bridge.time.removeClock(clockId) + } +} + +async function getAllAudioInputs () { + return (await audio.enumerateInputDevices()) + .map(device => ({ + id: device?.deviceId, + label: device?.label || 'Unnamed device' + })) +} + +async function getAudioDeviceWithId (deviceId) { + const devices = await getAllAudioInputs() + return devices.find(device => device.id === deviceId) +} + +function findTriggerCues (clockId) { + const items = bridge.state.getLocalState()?.items + const types = bridge.state.getLocalState()?._types + const typeIsTimecodeTriggerDict = {} + + if (typeof items !== 'object' || typeof types !== 'object') { + return + } + + return Object.entries(items) + .map(([id, item]) => { + if (Object.prototype.hasOwnProperty.call(typeIsTimecodeTriggerDict, item.type)) { + return [id, item, typeIsTimecodeTriggerDict[item.type]] + } + + const renderedType = bridge.types.renderType(item.type, types) + typeIsTimecodeTriggerDict[item.type] = item.type === TIMECODE_TRIGGER_TYPE || renderedType?.ancestors?.includes(TIMECODE_TRIGGER_TYPE) + return [id, item, typeIsTimecodeTriggerDict[item.type]] + }) + .filter(([,, typeIsTimecodeTrigger]) => { + return typeIsTimecodeTrigger + }) + /* + Filter to return only the + cues for the selected input + */ + .filter(([, item]) => { + const _clockId = getClockIdForInputLocal(item?.data?.timecode?.input) + return _clockId === clockId + }) + .map(([, item]) => item) +} + +function triggerCues (clockId, frame) { + const cues = cache.cache(`triggers:${clockId}`, () => findTriggerCues(clockId), TRIGGER_CUES_CACHE_TTL_MS) + if (!Array.isArray(cues)) { + return + } + + for (const cue of cues) { + const cueFrame = TimecodeFrame.fromSMPTE(cue?.data?.timecode?.smpte) + if (!cueFrame) { + continue + } + + if (TimecodeFrame.compare(frame, cueFrame)) { + bridge.items.playItem(cue.id) + } + } +} + +function submitFrameForClock (clockId, frame) { + triggerCues(clockId, frame) + bridge.time.submitFrame(clockId, frame) +} + +function ltcDeviceFactory (deviceId, frameRate = LTCDecoder.DEFAULT_FRAME_RATE_HZ, onFrame = () => {}) { + const device = DIController.instantiate('LTCDevice', { + LTCDecoder: DIController.instantiate('LTCDecoder', {}, + LTCDecoder.DEFAULT_SAMPLE_RATE_HZ, + frameRate, + LTCDecoder.DEFAULT_AUDIO_FORMAT + ) + }, { + deviceId + }, onFrame) + + device.start() + return device +} + +async function onLTCDeviceCreated (newSpec) { + /* + Make sure the input has registered + a clock in the Bridge API + */ + let clockId = await getClockIdForInput(newSpec?.id) + if (!clockId) { + clockId = await registerClockForInput(newSpec?.id, newSpec?.name) + } + + /* + Set up a new LTC device if there is already a specified device id, + this will be triggered if the workspace is loaded from disk and the + device has already been specified + */ + let device + if (newSpec?.deviceId) { + /* + Check if the device exists on this host as the project + file might have been loaded from another host + */ + const deviceExists = await getAudioDeviceWithId(newSpec.deviceId) + const frameRate = LTCDecoder.SUPPORTED_FRAME_RATES[newSpec?.frameRateIndex || 0] + + if (deviceExists) { + device = ltcDeviceFactory(newSpec?.deviceId, frameRate, frame => { + submitFrameForClock(clockId, frame) + }) + } + } + + LTC_DEVICES[newSpec?.id] = { + clockId, + device + } +} + +async function onLTCDeviceChanged (newSpec) { + if (!LTC_DEVICES[newSpec?.id]) { + return + } + + const device = LTC_DEVICES[newSpec?.id]?.device + const clockId = LTC_DEVICES[newSpec?.id]?.clockId + const frameRate = LTCDecoder.SUPPORTED_FRAME_RATES[newSpec?.frameRateIndex || 0] + + /* + Close and remove the current ltc + device if it is to be replaced + */ + if (device && !device?.compareTo({ ...newSpec, frameRate })) { + await device.close() + LTC_DEVICES[newSpec?.id].device = undefined + } + + /* + Create a new ltc device if none exists + and the spec isn't set to "no device" + */ + if ( + !LTC_DEVICES[newSpec?.id]?.device && + newSpec?.deviceId && + newSpec?.deviceId !== NO_AUDIO_DEVICE_ID && + clockId + ) { + LTC_DEVICES[newSpec?.id].device = ltcDeviceFactory(newSpec?.deviceId, newSpec?.frameRate, frame => { + submitFrameForClock(clockId, frame) + }) + } + + /* + Update the label of the clock feed + */ + if (clockId) { + await bridge.time.applyClock(clockId, { + label: newSpec?.name + }) + } +} + +async function onLTCDeviceRemoved (inputId) { + const spec = LTC_DEVICES[inputId] + + if (spec?.device) { + spec.device?.close() + } + + await removeClock(inputId, spec?.clockId) + + delete LTC_DEVICES[inputId] + logger.debug('Removed LTC device', inputId) +} + +async function updateDevicesFromSettings (inputs = []) { + for (const input of inputs) { + if (!LTC_DEVICES[input.id]) { + await onLTCDeviceCreated(input) + } else { + await onLTCDeviceChanged(input) + } + } + + /* + Close and remove devices that are no + longer specified in settings + */ + for (const deviceInputId of Object.keys(LTC_DEVICES)) { + const inputExists = inputs.find(input => input?.id === deviceInputId) + if (!inputExists) { + await onLTCDeviceRemoved(deviceInputId) + } + } +} + +/* +Activate the plugin and +bootstrap its contributions +*/ +exports.activate = async () => { + logger.debug('Activating timecode plugin') + + /* + Update the list of available audio devices + that's visible in settings + */ + { + const inputs = await getAllAudioInputs() + const inputSetting = await makeInputSetting(inputs) + const rescanSetting = makeRescanSetting(false) + const rescanSettingId = await bridge.settings.registerSetting(rescanSetting) + const inputsSettingId = await bridge.settings.registerSetting(inputSetting) + + async function rescanAudioDevices () { + /* + Show a loading indicator + next to the rescan button + */ + const loadingRescanSetting = makeRescanSetting(true) + bridge.settings.applySetting(rescanSettingId, loadingRescanSetting) + + const inputs = await getAllAudioInputs() + const inputSetting = await makeInputSetting(inputs, true) + + /* + Hide the loading indicator for the rescan + setting and update the inputs setting + */ + bridge.settings.applySetting(rescanSettingId, rescanSetting) + bridge.settings.applySetting(inputsSettingId, inputSetting) + } + + bridge.commands.registerCommand('timecode.rescanAudioDevices', rescanAudioDevices) + } + + /* + Update LTC devices whenever + the settings change + + This listener is also important for + the local state to stay updated, + removing this will prevent findTriggerCues + to work properly + */ + bridge.events.on('state.change', (state, set) => { + if (!set?.plugins?.[manifest?.name]?.settings?.ltc_devices) { + return + } + const inputs = state?.plugins?.[manifest?.name]?.settings?.ltc_devices || [] + updateDevicesFromSettings(inputs) + }) + + /* + Update LTC devices + on startup + */ + { + const initialInputs = await bridge.state.get(`plugins.${manifest?.name}.settings.ltc_devices`) + if (initialInputs) { + updateDevicesFromSettings(initialInputs) + } + } +} diff --git a/plugins/timecode/lib/Cache.js b/plugins/timecode/lib/Cache.js new file mode 100644 index 00000000..b6bad17c --- /dev/null +++ b/plugins/timecode/lib/Cache.js @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2022 Sveriges Television AB +// +// SPDX-License-Identifier: MIT + +const Logger = require('../../../lib/Logger') +const logger = new Logger({ name: 'Cache' }) + +const DEFAULT_ENTRY_LIFETIME_MS = 10000 + +class CacheEntry { + /** + * Get the immutable + * data stored by + * this entry + * @type { Any } + */ + get data () { + return this._data + } + + /** + * A boolean indicating whether + * or not this entry + * is still valid + * @type { Boolean } + */ + get isValid () { + return this._expires > Date.now() + } + + constructor (data, lifetime) { + /** + * @private + * A reference to the data + * stored in this entry + * @type { Any } + */ + this._data = data + + /** + * @private + * The time this entry + * expires in milliseconds + * @type { Number } + */ + this._expires = Date.now() + lifetime + } +} + +class Cache { + constructor () { + /** + * @private + * An key-value store + * holding cached data + * @type { Object. } + */ + this._store = {} + } + + /** + * Store some data in the cache + * @param { String } key A key used to identify the data + * @param { Any } data Any data to store + * @param { Number } lifetime The lifetime of the data in milliseconds + */ + store (key, data, lifetime = DEFAULT_ENTRY_LIFETIME_MS) { + logger.debug('Caching data for key', key) + const entry = new CacheEntry(data, lifetime) + this._store[key] = entry + } + + /** + * Get some data by its key, + * this will return valid data + * and data marked with keep=true + * @param { String } key The key used when storing the data + * @returns { Any? } + */ + get (key) { + const entry = this._store[key] + if (entry?.isValid) { + return entry?.data + } + this._store[key] = undefined + } + + /** + * Cache the return value + * of an async function, + * this is useful for expensive + * functions which should only + * be run if the cache has expired + * @param { String } key A key to identify the data + * @param { Function. } fn A function returning a promise + * resolving to some data to cache + * @param { Number } lifetime The lifetime of the data in milliseconds + * @returns + */ + cache (key, fn, lifetime) { + if (this._store[key]?.isValid) { + logger.debug('Cache hit for key', key) + return this._store[key].data + } + logger.debug('Cache miss for key', key) + + const res = fn() + this.store(key, res, lifetime) + return res + } + + /** + * Clear this cache's store and + * remove all stored entries + */ + clear () { + logger.debug('Clearing cache') + this._store = {} + } +} + +module.exports = Cache diff --git a/plugins/timecode/lib/DIController.js b/plugins/timecode/lib/DIController.js new file mode 100644 index 00000000..9f55f248 --- /dev/null +++ b/plugins/timecode/lib/DIController.js @@ -0,0 +1,2 @@ +const DIController = require('../../../shared/DIController') +module.exports = new DIController() diff --git a/plugins/timecode/lib/TimecodeDevice.js b/plugins/timecode/lib/TimecodeDevice.js new file mode 100644 index 00000000..cc5f9ba0 --- /dev/null +++ b/plugins/timecode/lib/TimecodeDevice.js @@ -0,0 +1,8 @@ +const DIBase = require('../../../shared/DIBase') + +class TimecodeDevice extends DIBase { + compareTo (spec) { + throw new Error('Subclass has not implemented the compareTo method, this is a requirement for all TimecodeDevice subclasses') + } +} +module.exports = TimecodeDevice diff --git a/plugins/timecode/lib/TimecodeFrame.js b/plugins/timecode/lib/TimecodeFrame.js new file mode 100644 index 00000000..53ab40fa --- /dev/null +++ b/plugins/timecode/lib/TimecodeFrame.js @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2026 Axel Boberg +// +// SPDX-License-Identifier: MIT + +/** + * @typedef {{ + * days: number, + * hours: number, + * minutes: number, + * seconds: number, + * frames: number, + * smpte: string + * }} RawTimecodeFrame + */ + +function zeroPad (n) { + if (n < 10) return `0${n}` + return `${n}` +} + +class TimecodeFrame { + /** + * Complete a full frame from a partial one + * such as returned by the LTC decoder + * + * All properties not existent in + * the partial object will be zeroed + * + * @param { any } partial + */ + static fromPartial (partial) { + if (typeof partial !== 'object') { + return undefined + } + + const out = { + days: parseInt(partial?.days || 0), + hours: parseInt(partial?.hours || 0), + minutes: parseInt(partial?.minutes || 0), + seconds: parseInt(partial?.seconds || 0), + frames: parseInt(partial?.frames || 0) + } + out.smpte = this.#makeSMPTEString(out.hours, out.minutes, out.seconds, out.frames) + return out + } + + /** + * Create an SMPTE string + * from separate values + * + * @param { number } hours + * @param { number } minutes + * @param { number } seconds + * @param { number } frames + * @returns { string } + */ + static #makeSMPTEString (hours, minutes, seconds, frames) { + return `${zeroPad(hours)}:${zeroPad(minutes)}:${zeroPad(seconds)}:${zeroPad(frames)}` + } + + /** + * Get a timecode frame + * from an SMPTE string + * @param { string } smpteString + * @returns { RawTimecodeFrame? } + */ + static fromSMPTE (smpteString) { + if (typeof smpteString !== 'string') { + return undefined + } + + const components = smpteString + // eslint-disable-next-line + .split(/[:\.;]+/) + .reverse() + + return { + days: parseInt(components[4] || 0), + hours: parseInt(components[3] || 0), + minutes: parseInt(components[2] || 0), + seconds: parseInt(components[1] || 0), + frames: parseInt(components[0] || 0), + smpte: smpteString + } + } + + /** + * Compare two timecode frames, + * will return true if they represent + * the same point in time + * @param { RawTimecodeFrame } frameA + * @param { RawTimecodeFrame } frameB + * @returns { boolean } + */ + static compare (frameA, frameB) { + if (typeof frameA !== 'object') { + return false + } + + if (typeof frameB !== 'object') { + return false + } + + return ( + frameA?.days === frameB?.days && + frameA?.hours === frameB?.hours && + frameA?.minutes === frameB?.minutes && + frameA?.seconds === frameB?.seconds && + frameA?.frames === frameB?.frames + ) + } +} +module.exports = TimecodeFrame diff --git a/plugins/timecode/lib/audio/index.js b/plugins/timecode/lib/audio/index.js new file mode 100644 index 00000000..612463d3 --- /dev/null +++ b/plugins/timecode/lib/audio/index.js @@ -0,0 +1,13 @@ +const { mediaDevices } = require('node-web-audio-api') + +async function enumerateDevices () { + const devices = await mediaDevices.enumerateDevices() + return devices +} +exports.enumerateDevices = enumerateDevices + +async function enumerateInputDevices () { + const devices = await enumerateDevices() + return devices.filter(device => device?.kind === 'audioinput') +} +exports.enumerateInputDevices = enumerateInputDevices diff --git a/plugins/timecode/lib/ltc/DataWorkletProcessor.js b/plugins/timecode/lib/ltc/DataWorkletProcessor.js new file mode 100644 index 00000000..4a5d2028 --- /dev/null +++ b/plugins/timecode/lib/ltc/DataWorkletProcessor.js @@ -0,0 +1,17 @@ +class DataWorkletProcessor extends AudioWorkletProcessor { + process (inputs, outputs, parameters) { + const input = inputs[0] + if (!input || input.length === 0) { + return true + } + + const channelData = input[0] + this.port.postMessage({ + buffer: channelData.slice() + }) + + return true + } +} + +registerProcessor('DataWorkletProcessor', DataWorkletProcessor) diff --git a/plugins/timecode/lib/ltc/LTCDecoder.js b/plugins/timecode/lib/ltc/LTCDecoder.js new file mode 100644 index 00000000..490d4082 --- /dev/null +++ b/plugins/timecode/lib/ltc/LTCDecoder.js @@ -0,0 +1,73 @@ +const { LTCDecoder: NativeLTCDecoder } = require('libltc-wrapper') +const DIController = require('../DIController') +const DIBase = require('../../../../shared/DIBase') + +const DEFAULT_SAMPLE_RATE_HZ = 48000 +const DEFAULT_FRAME_RATE_HZ = 25 +const DEFAULT_AUDIO_FORMAT = 'float' + +const SUPPORTED_FRAME_RATES = [24, 25, 30] + +class LTCDecoder extends DIBase { + #nativeDecoder + + static get DEFAULT_SAMPLE_RATE_HZ () { + return DEFAULT_SAMPLE_RATE_HZ + } + + static get DEFAULT_FRAME_RATE_HZ () { + return DEFAULT_FRAME_RATE_HZ + } + + static get DEFAULT_AUDIO_FORMAT () { + return DEFAULT_AUDIO_FORMAT + } + + static get SUPPORTED_FRAME_RATES () { + return SUPPORTED_FRAME_RATES + } + + /** + * Check if a provided frame rate + * is supported by the decoder + * @param { number } frameRate + * @returns { boolean } + */ + static isSupportedFrameRate (frameRate) { + return SUPPORTED_FRAME_RATES.includes(frameRate) + } + + #frameRate + + /** + * Get the frame rate + * of this decoder + * @type { number } + */ + get frameRate () { + return this.#frameRate + } + + constructor (props, sampleRate = DEFAULT_SAMPLE_RATE_HZ, frameRate = DEFAULT_FRAME_RATE_HZ, format = DEFAULT_AUDIO_FORMAT) { + super(props) + + let _frameRate = frameRate + if (!LTCDecoder.isSupportedFrameRate(frameRate)) { + _frameRate = LTCDecoder.DEFAULT_FRAME_RATE_HZ + } + + this.#frameRate = _frameRate + this.#nativeDecoder = new NativeLTCDecoder(sampleRate, _frameRate, format) + } + + write (buffer) { + this.#nativeDecoder.write(buffer) + } + + read () { + return this.#nativeDecoder.read() + } +} + +DIController.register('LTCDecoder', LTCDecoder) +module.exports = LTCDecoder diff --git a/plugins/timecode/lib/ltc/LTCDevice.js b/plugins/timecode/lib/ltc/LTCDevice.js new file mode 100644 index 00000000..d5d72d62 --- /dev/null +++ b/plugins/timecode/lib/ltc/LTCDevice.js @@ -0,0 +1,162 @@ +const { + mediaDevices, + AudioContext, + + // eslint-disable-next-line no-unused-vars + MediaStreamAudioSourceNode, + AudioWorkletNode +} = require('node-web-audio-api') + +const DIController = require('../DIController') +const TimecodeDevice = require('../TimecodeDevice') +const TimecodeFrame = require('../TimecodeFrame') + +const Logger = require('../../../../lib/Logger') +const logger = new Logger({ name: 'LTCDevice' }) + +require('./LTCDecoder') + +const DEFAULT_SAMPLE_RATE_HZ = 48000 + +/** + * @typedef {{ + * sampleRate: number, + * frameRate: number, + * deviceId: string + * }} LTCDeviceOptions + */ +class LTCDevice extends TimecodeDevice { + #audioContext + #onFrame + #opts + + #processor + #source + + /** + * Create a new ltc device + * @param { any[] } props + * @param { LTCDeviceOptions } opts + * @param { Function } onFrame + */ + constructor (props, opts = {}, onFrame = () => {}) { + super(props) + this.#opts = opts + this.#onFrame = onFrame + } + + /** + * Compare this device + * to a device spec obtained + * from Bridge settings + * + * Returning true indicates that + * the device matches the spec + * + * @param { any } spec + * @returns { boolean } + */ + compareTo (spec) { + return this.#opts?.deviceId === spec?.deviceId && + this.props.LTCDecoder.frameRate === spec?.frameRate + } + + #formatFrame (rawFrameData) { + return TimecodeFrame.fromPartial(rawFrameData) + } + + /** + * Decode audio data frame + * buffers into LTC timecode, + * + * this.#onFrame will be called + * for every timecode frame that's + * successfully decoded + * + * @param { Buffer } buffer + */ + #decodeAudioData (buffer) { + this.props.LTCDecoder.write(buffer) + + let frame = this.props.LTCDecoder.read() + while (frame) { + this.#onFrame(this.#formatFrame(frame)) + frame = this.props.LTCDecoder.read() + } + } + + async #setup () { + if (!this.#opts.deviceId) { + throw new Error('Missing device id as part of the required options object') + } + + this.#audioContext = new AudioContext({ + sampleRate: this.#opts?.sampleRate || DEFAULT_SAMPLE_RATE_HZ, + latencyHint: 'interactive' + }) + const ctx = this.#audioContext + + const mediaStream = await mediaDevices.getUserMedia({ + audio: { + deviceId: this.#opts?.deviceId, + echoCancellation: false, + autoGainControl: false, + noiseSuppression: false + } + }) + + const source = ctx.createMediaStreamSource(mediaStream) + + await ctx.audioWorklet.addModule('DataWorkletProcessor.js') + const processor = new AudioWorkletNode(ctx, 'DataWorkletProcessor') + + /* + Listen for incoming audio data + buffers from the processor and + decode them accordingly + */ + processor.port.onmessage = e => { + if (!e?.data?.buffer?.buffer) { + return + } + const buf = Buffer.from(e?.data?.buffer?.buffer) + this.#decodeAudioData(buf) + } + + source.connect(processor) + processor.connect(ctx.destination) + + /* + Keep references to the nodes in order to + properly tear them down in the close method + */ + this.#processor = processor + this.#source = source + } + + close () { + if (this.#audioContext) { + this.#audioContext.close() + this.#audioContext = undefined + } + + if (this.#processor?.port) { + this.#processor.port.onmessage = undefined + } + this.#processor?.disconnect() + this.#source?.disconnect() + logger.debug('Closed device') + } + + async start () { + if (!this.#audioContext) { + await this.#setup() + } + await this.#audioContext.resume() + logger.debug('Started device') + } +} + +DIController.register('LTCDevice', LTCDevice, [ + 'LTCDecoder' +]) diff --git a/plugins/timecode/package-lock.json b/plugins/timecode/package-lock.json new file mode 100644 index 00000000..f67e7885 --- /dev/null +++ b/plugins/timecode/package-lock.json @@ -0,0 +1,955 @@ +{ + "name": "bridge-plugin-timecode", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bridge-plugin-timecode", + "version": "1.0.0", + "license": "UNLICENSED", + "dependencies": { + "libltc-wrapper": "^1.1.2", + "node-web-audio-api": "^1.0.7" + }, + "devDependencies": { + "node-gyp": "^12.2.0" + }, + "engines": { + "bridge": "^0.0.1" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/cacache": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/caller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/caller/-/caller-1.1.0.tgz", + "integrity": "sha512-n+21IZC3j06YpCWaxmUy5AnVqhmCIM2bQtqQyy00HJlmStRt6kwDX5F9Z97pqwAB+G/tgSz6q/kUBbNyQzIubw==", + "license": "MIT" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/libltc-wrapper": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/libltc-wrapper/-/libltc-wrapper-1.1.2.tgz", + "integrity": "sha512-WbZocPhZZx8YQsye6zmRSCznEjmmsOmx9KTHt42RoyjLOr+lugkxv0wWyNum9QB17Lj8h9UbHA/27Cmp4FmnJQ==", + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", + "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.0.tgz", + "integrity": "sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-gyp": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-web-audio-api": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/node-web-audio-api/-/node-web-audio-api-1.0.7.tgz", + "integrity": "sha512-J1Po8THiPdh8ZOc6p8qrZau+DRK2HJkSyjGpaPGdbB5T+lugl2OmzSzc56nmESkWSEww9J30AjeAOdm94E5TBA==", + "license": "BSD-3-Clause", + "dependencies": { + "caller": "^1.1.0", + "node-fetch": "^3.3.2", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ssri": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/unique-filename": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", + "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/unique-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", + "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/plugins/timecode/package.json b/plugins/timecode/package.json new file mode 100644 index 00000000..0e91e97d --- /dev/null +++ b/plugins/timecode/package.json @@ -0,0 +1,73 @@ +{ + "name": "bridge-plugin-timecode", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "engines": { + "bridge": "^0.0.1" + }, + "keywords": [ + "bridge", + "plugin" + ], + "author": "Axel Boberg (axel.boberg@svt.se)", + "license": "UNLICENSED", + "dependencies": { + "libltc-wrapper": "^1.1.2", + "node-web-audio-api": "^1.0.7" + }, + "contributes": { + "types": [ + { + "id": "bridge.timecode.trigger", + "name": "Timecode trigger", + "inherits": "bridge.types.trigger", + "properties": { + "name": { + "default": "Timecode trigger" + }, + "timecode.input": { + "name": "Clock", + "type": "enum", + "enum": { + "$eval": { + "op": "concatArrays", + "a": [ + { + "label": "None", + "value": 0 + } + ], + "b": { + "$eval": { + "op": "arrayFromObject", + "path": "plugins.bridge-plugin-timecode.settings.ltc_devices", + "map": { + "label": "name", + "value": "id" + } + } + } + } + }, + "ui.group": "Timecode" + }, + "timecode.smpte": { + "name": "Timecode", + "type": "string", + "default": "10:00:00:00", + "allowsVariables": false, + "ui.group": "Timecode", + "ui.readable": true + } + } + } + ] + }, + "devDependencies": { + "node-gyp": "^12.2.0" + } +} diff --git a/plugins/types/index.js b/plugins/types/index.js index 7c2f6e7c..4d465368 100644 --- a/plugins/types/index.js +++ b/plugins/types/index.js @@ -18,23 +18,26 @@ const GROUP_PLAY_MODES = { first: 1 } -const PLAY_HANDLERS = { +const PLAY_HANDLERS = [ /* Trigger group children based on the group's play mode */ - 'bridge.types.group': item => { - switch (parseInt(item?.data?.playMode)) { - case GROUP_PLAY_MODES.first: - if (item?.children?.[0]) { - bridge.items.playItem(item?.children?.[0]) - } - break - case GROUP_PLAY_MODES.all: - default: - for (const child of (item?.children || [])) { - bridge.items.playItem(child) - } + { + predicate: item => item.type === 'bridge.types.group', + fn: item => { + switch (parseInt(item?.data?.playMode)) { + case GROUP_PLAY_MODES.first: + if (item?.children?.[0]) { + bridge.items.playItem(item?.children?.[0]) + } + break + case GROUP_PLAY_MODES.all: + default: + for (const child of (item?.children || [])) { + bridge.items.playItem(child) + } + } } }, @@ -42,33 +45,39 @@ const PLAY_HANDLERS = { Trigger a reference item's target */ - 'bridge.types.reference': item => { - if (!item?.data?.targetId) { - return - } - - switch (parseInt(item?.data?.playAction)) { - case types.REFERENCE_ACTION.none: - break - case types.REFERENCE_ACTION.stop: - bridge.items.stopItem(item?.data?.targetId) - break - case types.REFERENCE_ACTION.play: - default: - bridge.items.playItem(item?.data?.targetId) - break + { + predicate: (item, type) => item.type === 'bridge.types.reference' || type.ancestors.includes('bridge.types.reference'), + fn: item => { + if (!item?.data?.targetId) { + return + } + + switch (parseInt(item?.data?.playAction)) { + case types.REFERENCE_ACTION.none: + break + case types.REFERENCE_ACTION.stop: + bridge.items.stopItem(item?.data?.targetId) + break + case types.REFERENCE_ACTION.play: + default: + bridge.items.playItem(item?.data?.targetId) + break + } } } -} +] -const STOP_HANDLERS = { +const STOP_HANDLERS = [ /* Trigger group children based on the group's play mode */ - 'bridge.types.group': item => { - for (const child of (item?.children || [])) { - bridge.items.stopItem(child) + { + predicate: item => item.type === 'bridge.types.group', + fn: item => { + for (const child of (item?.children || [])) { + bridge.items.stopItem(child) + } } }, @@ -76,43 +85,49 @@ const STOP_HANDLERS = { Trigger a reference item's target */ - 'bridge.types.reference': item => { - if (!item?.data?.targetId) { - return - } - - switch (parseInt(item?.data?.stopAction)) { - case types.REFERENCE_ACTION.none: - break - case types.REFERENCE_ACTION.play: - bridge.items.playItem(item?.data?.targetId) - break - case types.REFERENCE_ACTION.stop: - default: - bridge.items.stopItem(item?.data?.targetId) - break + { + predicate: (item, type) => item.type === 'bridge.types.reference' || type.ancestors.includes('bridge.types.reference'), + fn: item => { + if (!item?.data?.targetId) { + return + } + + switch (parseInt(item?.data?.stopAction)) { + case types.REFERENCE_ACTION.none: + break + case types.REFERENCE_ACTION.play: + bridge.items.playItem(item?.data?.targetId) + break + case types.REFERENCE_ACTION.stop: + default: + bridge.items.stopItem(item?.data?.targetId) + break + } } } -} +] -const ITEM_CHANGE_HANDLERS = { +const ITEM_CHANGE_HANDLERS = [ /* Warn the user if a reference is targeting one of its own ancestors */ - 'bridge.types.reference': async item => { - const isAncestor = await utils.isAncestor(item?.data?.targetId, item?.id) - - if (!isAncestor) { - bridge.items.removeIssue(item?.id, 'types.rta') - return + { + predicate: (item, type) => item.type === 'bridge.types.reference' || type.ancestors.includes('bridge.types.reference'), + fn: async item => { + const isAncestor = await utils.isAncestor(item?.data?.targetId, item?.id) + + if (!isAncestor) { + bridge.items.removeIssue(item?.id, 'types.rta') + return + } + + bridge.items.applyIssue(item?.id, 'types.rta', { + description: 'Reference is targeting an ancestor, loops may occur' + }) } - - bridge.items.applyIssue(item?.id, 'types.rta', { - description: 'Reference is targeting an ancestor, loops may occur' - }) } -} +] async function initWidget () { const cssPath = `${assets.hash}.${manifest.name}.bundle.css` @@ -148,15 +163,26 @@ exports.activate = async () => { types.init(htmlPath) - bridge.events.on('item.play', item => { - PLAY_HANDLERS[item.type]?.(item) + function callHandlers (handlers, item, type) { + for (const handler of handlers) { + if (handler.predicate(item, type)) { + handler.fn(item, type) + } + } + } + + bridge.events.on('item.play', async item => { + const type = await bridge.types.getType(item.type) + callHandlers(PLAY_HANDLERS, item, type) }) - bridge.events.on('item.stop', item => { - STOP_HANDLERS[item.type]?.(item) + bridge.events.on('item.stop', async item => { + const type = await bridge.types.getType(item.type) + callHandlers(STOP_HANDLERS, item, type) }) - bridge.events.on('item.change', item => { - ITEM_CHANGE_HANDLERS[item?.type]?.(item) + bridge.events.on('item.change', async item => { + const type = await bridge.types.getType(item.type) + callHandlers(ITEM_CHANGE_HANDLERS, item, type) }) } diff --git a/plugins/types/lib/types.js b/plugins/types/lib/types.js index 27554a0a..a2080641 100644 --- a/plugins/types/lib/types.js +++ b/plugins/types/lib/types.js @@ -19,6 +19,9 @@ async function init (htmlPath) { name: 'Reference', inherits: 'bridge.types.delayable', properties: { + name: { + default: 'Reference' + }, playAction: { name: 'Play action', type: 'enum', diff --git a/plugins/types/package.json b/plugins/types/package.json index a8b28a08..fd5d7363 100644 --- a/plugins/types/package.json +++ b/plugins/types/package.json @@ -53,6 +53,9 @@ "id": "bridge.types.media", "inherits": "bridge.types.playable", "properties": { + "name": { + "default": "Media" + }, "duration": { "name": "Duration", "type": "string", @@ -73,6 +76,9 @@ "name": "Group", "inherits": "bridge.types.media", "properties": { + "name": { + "default": "Group" + }, "playMode": { "name": "Play mode", "type": "enum", @@ -86,6 +92,20 @@ "id": "bridge.types.divider", "name": "Divider", "inherits": "bridge.types.root" + }, + { + "id": "bridge.types.trigger", + "inherits": "bridge.types.reference", + "category": "Triggers", + "ui.icon": "trigger", + "properties": { + "name": { + "default": "Trigger" + }, + "color": { + "default": "#E543FF" + } + } } ], "settings": [ diff --git a/plugins/variables/package.json b/plugins/variables/package.json index 6ae445d0..9da18528 100644 --- a/plugins/variables/package.json +++ b/plugins/variables/package.json @@ -20,15 +20,23 @@ "name": "Variable", "inherits": "bridge.types.playable", "properties": { + "name": { + "default": "Variable" + }, + "color": { + "default": "#0F418C" + }, "variable.key": { - "name": "Variable name", + "name": "Key", "type": "string", - "ui.group": "Variable" + "ui.group": "Variable", + "ui.readable": true }, "variable.value": { - "name": "Variable value", + "name": "Value", "type": "string", - "ui.group": "Variable" + "ui.group": "Variable", + "ui.readable": true } } } diff --git a/scripts/electron-postbuild-macos.js b/scripts/electron-postbuild-macos.js new file mode 100644 index 00000000..221da44c --- /dev/null +++ b/scripts/electron-postbuild-macos.js @@ -0,0 +1,38 @@ +/** + * @description + * Post build actions for macOS-builds + * Run as `node electron-postbuild-macos.js ` + * + * Actions: + * - Copy dynamically linked libraries into the app bundle + */ + +const fs = require('node:fs') +const path = require('node:path') +const assert = require('node:assert') + +const [,, APP_BUNDLE] = process.argv +assert(APP_BUNDLE, 'Missing required argument \'app bundle\'') + +const CURRENT_DIR = process.cwd() + +const LIBRARIES_TO_COPY = [ + { + path: '../plugins/timecode/node_modules/libltc-wrapper/build/Release', + fileName: 'libltc.11.dylib' + } +] + +const FRAMEWORKS_DIR = path.join(CURRENT_DIR, APP_BUNDLE, '/Contents/Frameworks') + +/* +Copy libraries into the FRAMEWORKS_DIR +within the app bundle +*/ +console.log('Performing post build tasks') +for (const library of LIBRARIES_TO_COPY) { + const from = path.join(__dirname, library.path, library.fileName) + const to = path.join(FRAMEWORKS_DIR, library.fileName) + fs.copyFileSync(from, to) + console.log('%s\x1b[32m%s\x1b[0m', 'Copied framwork: ', library.fileName) +} diff --git a/scripts/electron-prebuild.js b/scripts/electron-prebuild.js new file mode 100644 index 00000000..9ce33945 --- /dev/null +++ b/scripts/electron-prebuild.js @@ -0,0 +1,52 @@ +/** + * @description + * Pre build actions for macOS-builds + * Run as `node electron-prebuild-macos.js ` + * + * Actions: + * - Rebuild native addons for all plugins + */ + +const cp = require('node:child_process') +const fs = require('node:fs') +const path = require('node:path') +const assert = require('node:assert') + +const [,, ARCH] = process.argv +assert(ARCH, 'Missing required argument \'arch\'') + +const PLUGINS_DIR = path.join(__dirname, '../plugins') +const MAIN_DIR = path.join(__dirname, '../') + +function forcePackageRebuild (path, arch) { + cp.execSync(`npm run env -- electron-rebuild -f -a ${arch}`, { + cwd: path + }) +} + +console.log('Performing pre build tasks') +fs.readdirSync(PLUGINS_DIR) + .map(pathname => ([path.join(PLUGINS_DIR, pathname), pathname])) + .filter(([pluginPath]) => { + /* + Filter out paths that are not directories, + since we keep a few other files in the plugins + directory as well + */ + return fs.statSync(pluginPath).isDirectory() + }) + .filter(([pluginPath]) => { + /* + Filter out paths that don't + contain a package.json file + */ + const packagePath = path.join(pluginPath, '/package.json') + return fs.existsSync(packagePath) + }) + .forEach(([pluginPath, pluginName]) => { + console.log('%s\x1b[32m%s\x1b[0m', 'Rebuilding addons for plugin: ', pluginName) + forcePackageRebuild(pluginPath, ARCH) + }) + +console.log('%s\x1b[32m%s\x1b[0m', 'Rebuilding addons for main codebase') +forcePackageRebuild(MAIN_DIR, ARCH) diff --git a/scripts/sign-macos.js b/scripts/sign-macos.js index a1e999f4..32204035 100644 --- a/scripts/sign-macos.js +++ b/scripts/sign-macos.js @@ -3,7 +3,15 @@ const path = require('node:path') function sign (path, label) { const opts = { - app: path + app: path, + binaries: [ + `${path}/Contents/Frameworks/libltc.11.dylib` + ], + optionsForFile: () => { + return { + hardenedRuntime: true + } + } } signAsync(opts) .then(() => {