Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
a8f948e
Move role and sharing functionality to a new footer
axelboberg Jan 10, 2026
67ba23c
Remove unused code
axelboberg Jan 10, 2026
2acca93
Add note regarding the footer to the changelog
axelboberg Jan 10, 2026
b6bf07d
Fix an issue where a comparison for the role determination was inverted
axelboberg Jan 10, 2026
61f98cf
Fix an issue resulting in some plugin settings not being rendered unt…
axelboberg Jan 11, 2026
a9b01bf
Allow apply operations using a dot-path
axelboberg Jan 11, 2026
7d7bbe0
Add a shortcut to open preferences
axelboberg Jan 11, 2026
8ba1d58
Start adding support for LTC timecode
axelboberg Jan 11, 2026
ccb1165
Add pre- and post-build scripts for managing native addons
axelboberg Jan 12, 2026
b82d9b5
Add support for lists in settings
axelboberg Jan 13, 2026
33af249
Add support for custom ids in select inputs in settings
axelboberg Jan 13, 2026
39a961a
Prevent the main window content from scrolling
axelboberg Jan 15, 2026
c9612f3
Clean up the timecode plugin
axelboberg Jan 17, 2026
e7d3559
Properly read the audio data buffers and decode them as LTC
axelboberg Jan 17, 2026
6af29cd
Remove unused code
axelboberg Jan 17, 2026
59e3da2
Start adding a new time api and work on the timecode plugin
axelboberg Jan 18, 2026
ec0ae09
Fix an issue where audio devices weren't listed correctly in settings…
axelboberg Jan 18, 2026
608cf01
Setup the state before loading the API as some APIs make use of it
axelboberg Jan 18, 2026
a80c93b
Minimize widget re-renders as much as possible
axelboberg Jan 18, 2026
f75e026
Fix typo
axelboberg Jan 18, 2026
963fcd3
Start rebuilding the clock widget to use the clock api
axelboberg Jan 18, 2026
b360884
Limit the number of times widgets are re-rendered
axelboberg Jan 18, 2026
c9080ed
Render nothing if no widget object is provided
axelboberg Jan 18, 2026
736d690
Memoize children using useEffect rather than useMemo to accomplish fa…
axelboberg Jan 18, 2026
cc1d608
Add better support for selecting clocks in the clock display widget
axelboberg Jan 19, 2026
8366fbe
Remove debug logs
axelboberg Jan 19, 2026
3ed7f19
Properly inherit certain functionality through type inheritance by tr…
axelboberg Jan 27, 2026
789dfb5
Remove trailing comma
axelboberg Jan 27, 2026
a8c3723
Add support for button inputs in preferences
axelboberg Jan 27, 2026
49c9344
Skip rendering undefined settings
axelboberg Jan 27, 2026
ccf64fe
Add support for button inputs in settings
axelboberg Jan 27, 2026
2361410
Add basic support for LTC triggers
axelboberg Jan 29, 2026
522540c
Add a loading indicator when searching for audio devices
axelboberg Jan 29, 2026
15dff93
Add a 'none' option for the time display widget and remove the second…
axelboberg Jan 29, 2026
2ab649b
Add styling to trigger cues and add support for ancestors in the rundown
axelboberg Jan 29, 2026
112b8bd
Clean up code to let TimecodeFrame handle all frame conversion
axelboberg Jan 30, 2026
fa16274
Update the design ov variable cues to include an icon and allow for c…
axelboberg Jan 30, 2026
d7c18fd
Decrease the margin of rundown list items marginally
axelboberg Jan 30, 2026
5c3568b
Remove asar to allow for code signing
axelboberg Jan 30, 2026
d3324fb
Fix an issue where plugin settings wasn't initiated on mount
axelboberg Jan 30, 2026
c913587
Force preference rerender on input change
axelboberg Jan 31, 2026
3d09e35
Fix an issue where settings wasn'r re-rendering properly on state change
axelboberg Jan 31, 2026
073f387
Fix an issue that led to clock references not being cleaned up proper…
axelboberg Jan 31, 2026
7e0a718
Redesign input switches
axelboberg Jan 31, 2026
11f8b6b
Fix an issue causing plugin settings to disappear on refresh
axelboberg Jan 31, 2026
b09e4a8
Merge pull request #4 from axelboberg/feat/ltc-timecode
axelboberg Jan 31, 2026
f30f7b7
Update dependencies
axelboberg Jan 31, 2026
06db3b1
Allow for granular defaults for types and set default values
axelboberg Jan 31, 2026
0dba918
Update changelog
axelboberg Jan 31, 2026
5beea8c
Update changelog
axelboberg Jan 31, 2026
b3662fb
Make sure the app menu waits for auth to be set before fetching data
axelboberg Feb 1, 2026
9546f18
Make context menus follow the color theme and tweak margins in the he…
axelboberg Feb 1, 2026
f6ad178
Update dependencies
axelboberg Feb 1, 2026
ceaf00a
Update dependencies
axelboberg Feb 1, 2026
0ea7940
Fix an issue where step inside on groups wouldn't re render the compo…
axelboberg Feb 1, 2026
f846eda
Update screenshot and readme
axelboberg Feb 1, 2026
274e2df
Update changelog
axelboberg Feb 1, 2026
5bf9af2
Update build instructions
axelboberg Feb 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ node_modules
assets.json
dist
bin
build

# temporary files
data
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion api/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require('./system')
require('./state')
require('./types')
require('./items')
require('./time')
require('./ui')

class API {
Expand All @@ -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
}
}
Expand All @@ -55,6 +57,7 @@ DIController.main.register('API', API, [
'State',
'Types',
'Items',
'Time',
'UI'
])

Expand Down
25 changes: 24 additions & 1 deletion api/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

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

const MissingArgumentError = require('./error/MissingArgumentError')
const InvalidArgumentError = require('./error/InvalidArgumentError')

class Settings {
#props

Expand All @@ -23,11 +26,31 @@ class Settings {
* Register a setting
* by its specification
* @param { SettingSpecification } specification A setting specification
* @returns { Promise.<Boolean> }
* @returns { Promise.<string> }
*/
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.<boolean> }
*/
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, [
Expand Down
125 changes: 122 additions & 3 deletions api/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.<State> }
Expand Down
39 changes: 39 additions & 0 deletions api/time.js
Original file line number Diff line number Diff line change
@@ -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'
])
56 changes: 44 additions & 12 deletions api/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down
Loading