Skip to content

Commit b86fc9c

Browse files
authored
Add LTC support and enhancements on Windows (#108)
* Move role and sharing functionality to a new footer Signed-off-by: Axel Boberg <git@axelboberg.se> * Remove unused code Signed-off-by: Axel Boberg <git@axelboberg.se> * Add note regarding the footer to the changelog Signed-off-by: Axel Boberg <git@axelboberg.se> * Fix an issue where a comparison for the role determination was inverted Signed-off-by: Axel Boberg <git@axelboberg.se> * Fix an issue resulting in some plugin settings not being rendered until the state has changed and been refetched Signed-off-by: Axel Boberg <git@axelboberg.se> * Allow apply operations using a dot-path Signed-off-by: Axel Boberg <git@axelboberg.se> * Add a shortcut to open preferences Signed-off-by: Axel Boberg <git@axelboberg.se> * Start adding support for LTC timecode Signed-off-by: Axel Boberg <git@axelboberg.se> * Add pre- and post-build scripts for managing native addons Signed-off-by: Axel Boberg <git@axelboberg.se> * Add support for lists in settings Signed-off-by: Axel Boberg <git@axelboberg.se> * Add support for custom ids in select inputs in settings Signed-off-by: Axel Boberg <git@axelboberg.se> * Prevent the main window content from scrolling Signed-off-by: Axel Boberg <git@axelboberg.se> * Clean up the timecode plugin Signed-off-by: Axel Boberg <git@axelboberg.se> * Properly read the audio data buffers and decode them as LTC Signed-off-by: Axel Boberg <git@axelboberg.se> * Remove unused code Signed-off-by: Axel Boberg <git@axelboberg.se> * Start adding a new time api and work on the timecode plugin Signed-off-by: Axel Boberg <git@axelboberg.se> * Fix an issue where audio devices weren't listed correctly in settings and add an option for placeholders to string inputs Signed-off-by: Axel Boberg <git@axelboberg.se> * Setup the state before loading the API as some APIs make use of it Signed-off-by: Axel Boberg <git@axelboberg.se> * Minimize widget re-renders as much as possible Signed-off-by: Axel Boberg <git@axelboberg.se> * Fix typo Signed-off-by: Axel Boberg <git@axelboberg.se> * Start rebuilding the clock widget to use the clock api Signed-off-by: Axel Boberg <git@axelboberg.se> * Limit the number of times widgets are re-rendered Signed-off-by: Axel Boberg <git@axelboberg.se> * Render nothing if no widget object is provided Signed-off-by: Axel Boberg <git@axelboberg.se> * Memoize children using useEffect rather than useMemo to accomplish faster load times Signed-off-by: Axel Boberg <git@axelboberg.se> * Add better support for selecting clocks in the clock display widget Signed-off-by: Axel Boberg <git@axelboberg.se> * Remove debug logs Signed-off-by: Axel Boberg <git@axelboberg.se> * Properly inherit certain functionality through type inheritance by tracking all ancestors Signed-off-by: Axel Boberg <git@axelboberg.se> * Remove trailing comma Signed-off-by: Axel Boberg <git@axelboberg.se> * Add support for button inputs in preferences Signed-off-by: Axel Boberg <git@axelboberg.se> * Skip rendering undefined settings Signed-off-by: Axel Boberg <git@axelboberg.se> * Add support for button inputs in settings Signed-off-by: Axel Boberg <git@axelboberg.se> * Add basic support for LTC triggers - Trigger cues - Evaluating state Signed-off-by: Axel Boberg <git@axelboberg.se> * Add a loading indicator when searching for audio devices Signed-off-by: Axel Boberg <git@axelboberg.se> * Add a 'none' option for the time display widget and remove the second tab in the startup template Signed-off-by: Axel Boberg <git@axelboberg.se> * Add styling to trigger cues and add support for ancestors in the rundown Signed-off-by: Axel Boberg <git@axelboberg.se> * Clean up code to let TimecodeFrame handle all frame conversion Signed-off-by: Axel Boberg <git@axelboberg.se> * Update the design ov variable cues to include an icon and allow for changing colors Signed-off-by: Axel Boberg <git@axelboberg.se> * Decrease the margin of rundown list items marginally Signed-off-by: Axel Boberg <git@axelboberg.se> * Remove asar to allow for code signing Signed-off-by: Axel Boberg <git@axelboberg.se> * Fix an issue where plugin settings wasn't initiated on mount Signed-off-by: Axel Boberg <git@axelboberg.se> * Force preference rerender on input change Signed-off-by: Axel Boberg <git@axelboberg.se> * Fix an issue where settings wasn'r re-rendering properly on state change Signed-off-by: Axel Boberg <git@axelboberg.se> * Fix an issue that led to clock references not being cleaned up properly by the timecode api Signed-off-by: Axel Boberg <git@axelboberg.se> * Redesign input switches Signed-off-by: Axel Boberg <git@axelboberg.se> * Fix an issue causing plugin settings to disappear on refresh Signed-off-by: Axel Boberg <git@axelboberg.se> * Update dependencies Signed-off-by: Axel Boberg <git@axelboberg.se> * Allow for granular defaults for types and set default values Signed-off-by: Axel Boberg <git@axelboberg.se> * Update changelog Signed-off-by: Axel Boberg <git@axelboberg.se> * Update changelog Signed-off-by: Axel Boberg <git@axelboberg.se> * Make sure the app menu waits for auth to be set before fetching data Signed-off-by: Axel Boberg <git@axelboberg.se> * Make context menus follow the color theme and tweak margins in the header on windows Signed-off-by: Axel Boberg <git@axelboberg.se> * Update dependencies Signed-off-by: Axel Boberg <git@axelboberg.se> * Update dependencies Signed-off-by: Axel Boberg <git@axelboberg.se> * Fix an issue where step inside on groups wouldn't re render the component Signed-off-by: Axel Boberg <git@axelboberg.se> * Update screenshot and readme Signed-off-by: Axel Boberg <git@axelboberg.se> * Update changelog Signed-off-by: Axel Boberg <git@axelboberg.se> * Update build instructions Signed-off-by: Axel Boberg <git@axelboberg.se> --------- Signed-off-by: Axel Boberg <git@axelboberg.se>
1 parent 557c69f commit b86fc9c

119 files changed

Lines changed: 5114 additions & 655 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ node_modules
88
assets.json
99
dist
1010
bin
11+
build
1112

1213
# temporary files
1314
data

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,21 @@
55
- Support for named urls when sharing links to workspaces
66
- Ability to convert items to other types by right-clicking
77
- Ancestor items in context menus now stay tinted when their child menus are opened
8+
- A shortcut to open preferences (CMD/CTRL+,)
9+
- Support for lists in settings
10+
- Support for custom ids in select inputs in settings
11+
- Support for LTC timecode and triggers
12+
- A state evaluation API
13+
- Granular type inheritance
14+
- Default names to types
15+
### Changed
16+
- Some features have moved to the footer of the app window
17+
- Context menus now follow the color theme
18+
- Windows builds now use a custom window header
819
### Fixed
920
- An issue where the inspector started to scroll horisontally on overflow
1021
- Closing electron windows may cause a loop preventing user defaults from being saved
22+
- An issue where settings didn't render after reload
1123

1224
## 1.0.0-beta.8
1325
### Fixed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ The roadmap is available on Notion
4444
- OSC API and triggers
4545
- HTTP triggers
4646
- CasparCG library, playout and templates
47+
- LTC timecode triggers
4748

4849
## Community plugins
4950
- [CRON - triggers based on the time of day](https://github.com/axelboberg/bridge-plugin-cron)

api/events.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,9 @@ class Events {
202202
* @param { EventHandler } handler A handler to remove
203203
*/
204204
off (event, handler) {
205-
if (!this.localHandlers.has(event)) return
205+
if (!this.localHandlers.has(event)) {
206+
return
207+
}
206208

207209
const handlers = this.localHandlers.get(event)
208210
const index = handlers.findIndex(({ handler: _handler }) => _handler === handler)

api/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require('./system')
1818
require('./state')
1919
require('./types')
2020
require('./items')
21+
require('./time')
2122
require('./ui')
2223

2324
class API {
@@ -36,6 +37,7 @@ class API {
3637
this.state = props.State
3738
this.types = props.Types
3839
this.items = props.Items
40+
this.time = props.Time
3941
this.ui = props.UI
4042
}
4143
}
@@ -55,6 +57,7 @@ DIController.main.register('API', API, [
5557
'State',
5658
'Types',
5759
'Items',
60+
'Time',
5861
'UI'
5962
])
6063

api/settings.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

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

15+
const MissingArgumentError = require('./error/MissingArgumentError')
16+
const InvalidArgumentError = require('./error/InvalidArgumentError')
17+
1518
class Settings {
1619
#props
1720

@@ -23,11 +26,31 @@ class Settings {
2326
* Register a setting
2427
* by its specification
2528
* @param { SettingSpecification } specification A setting specification
26-
* @returns { Promise.<Boolean> }
29+
* @returns { Promise.<string> }
2730
*/
2831
registerSetting (specification) {
2932
return this.#props.Commands.executeCommand('settings.registerSetting', specification)
3033
}
34+
35+
/**
36+
* Apply changes to a registered
37+
* setting in the state
38+
*
39+
* @param { String } id The id of a setting to update
40+
* @param { SettingSpecification } set A setting object to apply
41+
* @returns { Promise.<boolean> }
42+
*/
43+
async applySetting (id, set = {}) {
44+
if (typeof id !== 'string') {
45+
throw new MissingArgumentError('Invalid value for item id, must be a string')
46+
}
47+
48+
if (typeof set !== 'object' || Array.isArray(set)) {
49+
throw new InvalidArgumentError('Argument \'set\' must be a valid object that\'s not an array')
50+
}
51+
52+
return this.#props.Commands.executeCommand('settings.applySetting', id, set)
53+
}
3154
}
3255

3356
DIController.main.register('Settings', Settings, [

api/state.js

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,53 @@ const merge = require('../shared/merge')
77
const Cache = require('./classes/Cache')
88
const DIController = require('../shared/DIController')
99

10+
const objectPath = require('object-path')
11+
1012
const CACHE_MAX_ENTRIES = 10
1113

14+
function mapObject (obj, map) {
15+
if (typeof map !== 'object' || typeof obj !== 'object') {
16+
return obj
17+
}
18+
19+
const out = {}
20+
for (const [key, path] of Object.entries(map)) {
21+
if (typeof path !== 'string') {
22+
continue
23+
}
24+
out[key] = obj[path]
25+
}
26+
return out
27+
}
28+
29+
const EVALUATION_OPERATIONS = {
30+
arrayFromObject: (opts, data) => {
31+
if (typeof opts?.path !== 'string') {
32+
return
33+
}
34+
35+
const obj = objectPath.get(data, opts.path)
36+
if (!obj) {
37+
return
38+
}
39+
40+
return Object.entries(obj)
41+
.map(([, value]) => {
42+
let _value = value
43+
if (typeof value !== 'object') {
44+
_value = { value }
45+
}
46+
return mapObject(_value, opts?.map)
47+
})
48+
},
49+
concatArrays: (opts, data) => {
50+
if (!Array.isArray(opts?.a) || !Array.isArray(opts?.b)) {
51+
return opts?.a
52+
}
53+
return [...opts.a, ...opts.b]
54+
}
55+
}
56+
1257
class State {
1358
#props
1459

@@ -117,24 +162,98 @@ class State {
117162
}
118163
}
119164

165+
/**
166+
* Create a new object by expanding the path
167+
* and set the provided value
168+
* @param { string } path
169+
* @param { any } value
170+
* @param { string | undefined } delimiter
171+
* @returns { any }
172+
*/
173+
#expandObjectPath (path, value, delimiter = '.') {
174+
const parts = path.split(delimiter)
175+
176+
const out = {}
177+
let pointer = out
178+
179+
for (let i = 0; i < parts.length; i++) {
180+
const key = parts[i]
181+
if (i === parts.length - 1) {
182+
pointer[key] = value
183+
} else {
184+
pointer[key] = {}
185+
pointer = pointer[key]
186+
}
187+
}
188+
189+
return out
190+
}
191+
120192
/**
121193
* Apply some data to the state,
122194
* most often this function shouldn't
123195
* be called directly - there's probably
124196
* a command for what you want to do
125-
* @param { Object } set Data to apply to the state
197+
* @param { object } set Data to apply to the state
126198
*//**
127199
* Apply some data to the state,
128200
* most often this function shouldn't
129201
* be called directly - there's probably
130202
* a command for what you want to do
131-
* @param { Object[] } set An array of data objects to
203+
* @param { object[] } set An array of data objects to
132204
* apply to the state in order
205+
*//**
206+
* Apply some data to the state,
207+
* most often this function shouldn't
208+
* be called directly - there's probably
209+
* a command for what you want to do
210+
* @param { string } path A dot-path to which the value will be applied
211+
* @param { object } set A value to apply
133212
*/
134-
apply (set) {
213+
apply (arg0, arg1) {
214+
let set = arg0
215+
216+
/*
217+
If the function received a path and a value,
218+
expand create an object that can be set directly
219+
*/
220+
if (typeof arg0 === 'string' && arg1) {
221+
set = this.#expandObjectPath(arg0, arg1)
222+
}
223+
135224
this.#props.Commands.executeRawCommand('state.apply', set)
136225
}
137226

227+
#evaluateProperty (propertyFieldEvaluation, dataDict = {}) {
228+
const op = propertyFieldEvaluation?.op
229+
if (!op || !EVALUATION_OPERATIONS[op]) {
230+
return
231+
}
232+
return EVALUATION_OPERATIONS[op](propertyFieldEvaluation, dataDict)
233+
}
234+
235+
async evaluate (obj, data) {
236+
if (typeof obj !== 'object' || !obj) {
237+
return obj
238+
}
239+
240+
let _data = data
241+
if (!_data) {
242+
_data = this.getLocalState() || await this.get()
243+
}
244+
245+
for (const key of Object.keys(obj)) {
246+
obj[key] = await this.evaluate(obj[key], _data)
247+
}
248+
249+
if (obj?.$eval) {
250+
const newObj = this.#evaluateProperty(obj.$eval, _data)
251+
return this.evaluate(newObj, _data)
252+
}
253+
254+
return obj
255+
}
256+
138257
/**
139258
* Get the full current state
140259
* @returns { Promise.<State> }

api/time.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// SPDX-FileCopyrightText: 2026 Axel Boberg
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
const DIController = require('../shared/DIController')
6+
7+
class Time {
8+
#props
9+
10+
constructor (props) {
11+
this.#props = props
12+
}
13+
14+
getAllClocks () {
15+
return this.#props.Commands.executeCommand('time.getAllClocks')
16+
}
17+
18+
registerClock (spec) {
19+
return this.#props.Commands.executeCommand('time.registerClock', spec)
20+
}
21+
22+
removeClock (id) {
23+
return this.#props.Commands.executeCommand('time.removeClock', id)
24+
}
25+
26+
applyClock (id, set) {
27+
return this.#props.Commands.executeCommand('time.applyClock', id, set)
28+
}
29+
30+
submitFrame (id, frame) {
31+
return this.#props.Commands.executeRawCommand('time.submitFrame', id, frame)
32+
}
33+
}
34+
35+
DIController.main.register('Time', Time, [
36+
'State',
37+
'Events',
38+
'Commands'
39+
])

api/types.js

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,50 @@
55
const Cache = require('./classes/Cache')
66
const DIController = require('../shared/DIController')
77

8+
const utils = require('./utils')
9+
810
const CACHE_MAX_ENTRIES = 100
911

12+
function shallowMergeObjects (a, b) {
13+
if (typeof a !== 'object' || typeof b !== 'object') {
14+
return b
15+
}
16+
17+
return {
18+
...a,
19+
...b
20+
}
21+
}
22+
23+
/*
24+
Export for testing only
25+
*/
26+
exports.shallowMergeObjects = shallowMergeObjects
27+
1028
/**
11-
* Perform a deep clone
12-
* of an object
13-
* @param { any } obj An object to clone
29+
* Merge all properties two level deep
30+
* from two types
31+
* @param { any } a
32+
* @param { any } b
1433
* @returns { any }
1534
*/
16-
function deepClone (obj) {
17-
if (typeof window !== 'undefined' && window.structuredClone) {
18-
return window.structuredClone(obj)
35+
function mergeProperties (a, b) {
36+
const out = { ...a }
37+
for (const key of Object.keys(b)) {
38+
if (Object.prototype.hasOwnProperty.call(out, key)) {
39+
out[key] = shallowMergeObjects(a[key], b[key])
40+
} else {
41+
out[key] = b[key]
42+
}
1943
}
20-
return JSON.parse(JSON.stringify(obj))
44+
return out
2145
}
2246

47+
/*
48+
Export for testing only
49+
*/
50+
exports.mergeProperties = mergeProperties
51+
2352
class Types {
2453
#props
2554

@@ -43,7 +72,8 @@ class Types {
4372
renderType (id, typesDict = {}) {
4473
if (!typesDict[id]) return undefined
4574

46-
const type = deepClone(typesDict[id])
75+
const type = utils.deepClone(typesDict[id])
76+
type.ancestors = []
4777

4878
/*
4979
Render the ancestor if this
@@ -52,10 +82,12 @@ class Types {
5282
if (type.inherits) {
5383
const ancestor = this.renderType(type.inherits, typesDict)
5484

55-
type.properties = {
56-
...ancestor?.properties || {},
57-
...type.properties || {}
58-
}
85+
type.ancestors = [...(ancestor?.ancestors || []), type.inherits]
86+
type.category = type.category || ancestor?.category
87+
type.properties = mergeProperties(
88+
(ancestor?.properties || {}),
89+
(type?.properties || {})
90+
)
5991
}
6092

6193
return type

0 commit comments

Comments
 (0)