Skip to content

Commit

Permalink
v1.0.0-beta.2 (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboberg authored Feb 12, 2025
1 parent 05f7518 commit e3eca00
Show file tree
Hide file tree
Showing 33 changed files with 431 additions and 163 deletions.
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<img src="./media/appicon.png" width="90px">

# Bridge
![Test](https://github.com/svt/bridge/actions/workflows/.github/workflows/test.yml/badge.svg?branch=main)
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
[![REUSE status](https://api.reuse.software/badge/github.com/svt/bridge)](https://api.reuse.software/info/github.com/svt/bridge)

Extendable and lightweight graphics playout software
Extendable and lightweight playout software

![Screenshot](/media/screenshot.png)

Expand All @@ -29,10 +31,16 @@ As developers of production software we found ourselves in a position of rebuild

## Features

- [ ] Core features are bundled (rundown, default types, clock, osc e.t.c.)
- [ ] Can be run both as a desktop app and a cloud deployment
- [ ] Can be used by many operators simultaneously with real time sync
- [ ] Real-time sync for multiple operators
- [ ] A fully customizable grid layout
- [ ] Variables
- [ ] Item references
- [ ] Sub-frame accurate timing
- [ ] Multi-threaded architecture
- [ ] Nested groups
- [ ] Multiple rundowns per project
- [ ] Shotbox-style buttons
- [ ] OSC API

## Download and install
Built binaries are available on the releases page.
Expand All @@ -52,13 +60,10 @@ Please see our security policy for instructions on how to report security issues

## License

Bridge source code is released under the:

[MIT License](LICENSE.md)
Bridge source code is released under the [MIT License](LICENSE.md)

Most of the other material as icons are relased under a Creative Commons License, see .reuse/dep5 for further information about them.


----

## Primary Maintainer
Expand Down
44 changes: 30 additions & 14 deletions api/browser/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,31 @@ const InvalidArgumentError = require('../error/InvalidArgumentError')
const LazyValue = require('../classes/LazyValue')
const DIController = require('../../shared/DIController')

/**
* @typedef {{
* id: String,
* role: Number,
* heartbeat: Number,
* isPersistent: Boolean,
* isEditingLayout: Boolean
* }} Connection
*
* @typedef {{
* caller: String
* }} ClientSelectionState
*/

/**
* The default state object,
* if nothing else is specified,
* for the 'selection' event
*
* @type { ClientSelectionState }
*/
const DEFAULT_SELECTION_EVENT_STATE = {
caller: undefined
}

/**
* @private
* Ensure that a 'thing' is an array,
Expand All @@ -25,16 +50,6 @@ function ensureArray (thing) {
return arr
}

/**
* @typedef {{
* id: String,
* role: Number,
* heartbeat: Number,
* isPersistent: Boolean,
* isEditingLayout: Boolean
* }} Connection
*/

class Client {
#props

Expand Down Expand Up @@ -110,8 +125,9 @@ class Client {
* will replace the
* current selection
* @param { String[] } item Multiple items to select
* @param { ClientSelectionState } state An optional state to pass with the event
*/
async setSelection (item) {
async setSelection (item, state = DEFAULT_SELECTION_EVENT_STATE) {
this.assertIdentity()

const items = ensureArray(item)
Expand All @@ -123,7 +139,7 @@ class Client {
}
})

this.#props.Events.emitLocally('selection', items)
this.#props.Events.emitLocally('selection', items, state)
}

/**
Expand Down Expand Up @@ -156,7 +172,7 @@ class Client {
}
}
})
this.#props.Events.emitLocally('selection', newSelection)
this.#props.Events.emitLocally('selection', newSelection, DEFAULT_SELECTION_EVENT_STATE)
}

/**
Expand Down Expand Up @@ -211,7 +227,7 @@ class Client {
}
})

this.#props.Events.emitLocally('selection', [])
this.#props.Events.emitLocally('selection', [], DEFAULT_SELECTION_EVENT_STATE)
}

/**
Expand Down
42 changes: 21 additions & 21 deletions api/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ const DIController = require('../shared/DIController')

/**
* @typedef {{
* callee: String
* caller: String
* }} EventHandlerOpts
*
* @property { String } callee An optional identifier for the
* callee of the function,
* @property { String } caller An optional identifier for the
* caller of the function,
* this is used to clean up handlers
* when a frame is no longer being used
*/
Expand Down Expand Up @@ -123,7 +123,7 @@ class Events {
if (Array.isArray(res)) return res
return [res]
}
appendToMapArray(this.intercepts, event, { fn, callee: opts?.callee || this.opts?.callee })
appendToMapArray(this.intercepts, event, { fn, caller: opts?.caller || this.opts?.caller })
}

/**
Expand Down Expand Up @@ -157,7 +157,7 @@ class Events {
* @returns { Promise.<void> }
*/
async on (event, handler, opts) {
appendToMapArray(this.localHandlers, event, { handler, callee: opts?.callee || this.opts?.callee })
appendToMapArray(this.localHandlers, event, { handler, caller: opts?.caller || this.opts?.caller })

/*
Only setup the command if
Expand Down Expand Up @@ -233,15 +233,15 @@ class Events {
* Remove all listeners
*//**
* Remove all listeners associated
* with the specified callee
* @param { String } callee
* with the specified caller
* @param { String } caller
* @returns { Number } The number of listeners that were removed
*/
removeAllListeners (callee) {
removeAllListeners (caller) {
let count = 0
for (const event of this.localHandlers.keys()) {
for (const { handler, callee: _callee } of this.localHandlers.get(event)) {
if (callee && _callee !== callee) {
for (const { handler, caller: _caller } of this.localHandlers.get(event)) {
if (caller && _caller !== caller) {
continue
}
this.off(event, handler)
Expand All @@ -255,15 +255,15 @@ class Events {
* Remove all this.intercepts
*//**
* Remove all this.intercepts associated
* with the specified callee
* @param { String } callee
* with the specified caller
* @param { String } caller
* @returns { Number } The number of intercepts that were removed
*/
removeAllIntercepts (callee) {
removeAllIntercepts (caller) {
let count = 0
for (const event of this.intercepts.keys()) {
for (const { fn, callee: _callee } of this.intercepts.get(event)) {
if (callee && _callee !== callee) {
for (const { fn, caller: _caller } of this.intercepts.get(event)) {
if (caller && _caller !== caller) {
continue
}
this.removeIntercept(event, fn)
Expand Down Expand Up @@ -292,12 +292,12 @@ class Events {
* 'removeAllListeners' and 'removeAllIntercepts'
* methods
*
* @param { String } callee A unique id that can be associated
* @param { String } caller A unique id that can be associated
* with calls made by the scope
*
* @returns { Proxy.<Events> }
*/
createScope (callee) {
createScope (caller) {
/*
Create a scope object with methods
that will override the original
Expand All @@ -308,26 +308,26 @@ class Events {
original instance
*/
const scope = {}
scope.id = callee
scope.id = caller

scope.intercept = (event, handler, opts) => {
return this.intercept(event, handler, {
...opts,
callee
caller
})
}

scope.on = (event, handler, opts) => {
return this.on(event, handler, {
...opts,
callee
caller
})
}

scope.once = (event, handler, opts) => {
return this.once(event, handler, {
...opts,
callee
caller
})
}

Expand Down
18 changes: 9 additions & 9 deletions api/events.unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ beforeAll(() => {
})
})

test('create a new callee scope', () => {
const scope = events.createScope('myCallee')
expect(scope.id).toEqual('myCallee')
test('create a new caller scope', () => {
const scope = events.createScope('mycaller')
expect(scope.id).toEqual('mycaller')
})

test('remove all listeners for a callee', () => {
const scope = events.createScope('mySecondCallee')
test('remove all listeners for a caller', () => {
const scope = events.createScope('mySecondcaller')
scope.on('test', () => {})
expect(events.removeAllListeners('mySecondCallee')).toEqual(1)
expect(events.removeAllListeners('mySecondcaller')).toEqual(1)
})

test('remove all intercepts for a callee', () => {
const scope = events.createScope('myThirdCallee')
test('remove all intercepts for a caller', () => {
const scope = events.createScope('myThirdcaller')
scope.intercept('test', () => {})
expect(events.removeAllIntercepts('myThirdCallee')).toEqual(1)
expect(events.removeAllIntercepts('myThirdcaller')).toEqual(1)
})
36 changes: 34 additions & 2 deletions api/variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,22 @@ const objectPath = require('object-path')

const DIController = require('../shared/DIController')

const VARIABLE_REGEX = /\$\((.*?)\)/g
/**
* The regex used to match
* variables in strings
*
* @example
* "My string $(my_variable)" -> MATCH
* "My string no variable" -> NO MATCH
*
* This should NOT be made global "/g" as that will trigger
* an issue where the expression will only match every
* other time it's used – this is known behaviour in
* multiple browsers
*
* @type { RegExp }
*/
const VARIABLE_REGEX = /\$\((.*?)\)/

class Variables {
#props
Expand All @@ -15,6 +30,19 @@ class Variables {
this.#props = props
}

/**
* Check if a string contains
* at least one variable
* @param { String } str
* @returns { Boolean }
*/
stringContainsVariable (str) {
if (typeof str !== 'string') {
return false
}
return VARIABLE_REGEX.test(str)
}

/**
* Set a variable's value
* @param { String } key
Expand Down Expand Up @@ -56,7 +84,11 @@ class Variables {
* @returns { String }
*/
substituteInString (str, data = (this.#props.State.getLocalState()?.variables || {}), overrideData = {}) {
const text = str.split(VARIABLE_REGEX)
if (!str) {
return ''
}

const text = `${str}`.split(VARIABLE_REGEX)
const values = {
...data,
...overrideData
Expand Down
9 changes: 9 additions & 0 deletions api/variables.unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,12 @@ test('substitutes variables in a string without spaces', () => {
const data = { foo: 'Hello', bar: 'World' }
expect(variables.substituteInString(str, data)).toEqual('HelloWorld')
})

test('check if a string contains one or more variables', () => {
const str1 = '$(foo)$(bar)'
const str2 = 'Hello World'
const nonString = 23
expect(variables.stringContainsVariable(str1)).toEqual(true)
expect(variables.stringContainsVariable(str2)).toEqual(false)
expect(variables.stringContainsVariable(nonString)).toEqual(false)
})
2 changes: 1 addition & 1 deletion app/components/ContextMenu/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const ContextMenu = ({ x, y, children, onClose = () => {} }) => {

window.addEventListener('blur', closeContext)
window.addEventListener('click', closeContext)
window.addEventListener('contextmenu', closeContext, true)
window.addEventListener('contextmenu', closeContext)
return () => {
window.removeEventListener('blur', closeContext)
window.removeEventListener('click', closeContext)
Expand Down
Loading

0 comments on commit e3eca00

Please sign in to comment.