Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"build": "turbo run build",
"build-saas": "turbo run build-saas",
"dev": "npm run dev-dev --prefix packages/springboard/cli",
"dev-hands": "npm run dev-dev --prefix packages/springboard/cli -- ./apps/small_apps/handraiser/index.ts",
"docs": "cd docs && docker compose up",
"dev-without-node": "turbo run dev --filter=\"!./apps/jamtools/node\"",
"debug-node": "npm run debug --prefix apps/jamtools/node",
Expand Down
126 changes: 91 additions & 35 deletions packages/jamtools/features/modules/hand_raiser_module.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,115 @@
import {MidiControlChangeInputResult} from '@jamtools/core/modules/macro_module/macro_handlers/inputs/midi_control_change_input_macro_handler';
import React from 'react';
import React, {useEffect, useMemo, useState} from 'react';

import springboard from 'springboard';

import './hand_raiser.css';

type UserData = {
userId: string;
connected: boolean;
name?: string;
handPosition: number;
}

springboard.registerModule('HandRaiser', {}, async (m) => {
const macroModule = m.getModule('macro');
macroModule.setLocalMode(true);

const states = await m.createStates({
handPositions: [0, 0],
roomStateV4: {} as Record<string, UserData>,
});

const myHandPosition = await m.statesAPI.createUserAgentState('myHandPosition', 0);

m.hooks.onUserConnect((user, users) => {
console.log('onUserConnect', user, users);
states.roomStateV4.setStateImmer((state) => {
for (const key of Object.keys(state)) {
if (!users.find(u => u.id === key)) {
delete state[key];
}
}

state[user.id] = {
userId: user.id,
name: user.id,
handPosition: state[user.id]?.handPosition || 0,
connected: true,
};
});
});

m.hooks.onUserDisconnect((user, users) => {
// console.log('onUserDisconnect', user, users);
states.roomStateV4.setStateImmer((state) => {
delete state[user.id];
});
});

const actions = m.createActions({
changeHandPosition: async (args: {index: number, value: number}) => {
states.handPositions.setStateImmer((positions) => {
positions[args.index] = args.value;
changeHandPosition: async (args: {value: number}, options, userData) => {
if (!userData?.userId) {
return;
}

states.roomStateV4.setStateImmer((users) => {
users[userData.userId].handPosition = args.value;
});
},
getMyUserId: async (args, options, userData) => {
console.log('getMyUserId', args, options, userData);
return {
userId: userData?.userId || '',
};
},
});

let originalMyId = '';
if (m.deps.core.rpc.remote.role === 'client') {
originalMyId = (await actions.getMyUserId()).userId;
}

const macros = await macroModule.createMacros(m, {
slider0: {
handMovingSlider: {
type: 'midi_control_change_input',
config: {
onTrigger: (midiEvent => {
if (midiEvent.event.value) {
actions.changeHandPosition({index: 0, value: midiEvent.event.value});
}
}),
}
},
slider1: {
type: 'midi_control_change_input',
config: {
onTrigger: (midiEvent => {
if (midiEvent.event.value) {
actions.changeHandPosition({index: 1, value: midiEvent.event.value});
actions.changeHandPosition({value: midiEvent.event.value});
}
}),
}
},
});

m.registerRoute('/', {}, () => {
const positions = states.handPositions.useState();
const roomState = states.roomStateV4.useState();
const [myId, setMyId] = useState(originalMyId);

const connectedUsers = useMemo(() => Object.values(roomState).filter(u => u.connected), [roomState]);

useEffect(() => {
(async () => {
setMyId((await actions.getMyUserId()).userId);
})();
}, [connectedUsers]);

return (
<div className='hand-raiser-main'>
<div className='hand-raiser-center'>
{positions.map((position, index) => (
<HandSliderContainer
key={index}
position={position}
handlePositionChange={value => actions.changeHandPosition({index, value})}
macro={index === 0 ? macros.slider0 : macros.slider1}
/>
<pre>{myId}</pre>
{connectedUsers.map((user) => (
<>
<HandSliderContainer
key={user.userId}
position={user.handPosition}
handlePositionChange={value => actions.changeHandPosition({value})}
macro={macros.handMovingSlider}
showSlider={user.userId === myId}
/>
<pre>{JSON.stringify(user, null, 2)}</pre>
</>
))}
</div>
</div>
Expand All @@ -68,6 +121,7 @@ type HandRaiserModuleProps = {
position: number;
handlePositionChange: (position: number) => void;
macro: MidiControlChangeInputResult;
showSlider: boolean;
};

const HandSliderContainer = (props: HandRaiserModuleProps) => {
Expand All @@ -76,16 +130,18 @@ const HandSliderContainer = (props: HandRaiserModuleProps) => {
<Hand
position={props.position}
/>
<div className='slider-container'>
<Slider
value={props.position}
onChange={props.handlePositionChange}
/>
<details style={{cursor: 'pointer'}}>
<summary>Configure MIDI</summary>
<props.macro.components.edit />
</details>
</div>
{props.showSlider && (
<div className='slider-container'>
<Slider
value={props.position}
onChange={props.handlePositionChange}
/>
<details style={{cursor: 'pointer'}}>
<summary>Configure MIDI</summary>
<props.macro.components.edit />
</details>
</div>
)}
</div>
);
};
Expand Down
38 changes: 36 additions & 2 deletions packages/springboard/core/engine/engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export class Springboard {
api: ModuleReturnValue
}> => {
const mod: Module = {moduleId};
const moduleAPI = new ModuleAPI(mod, 'engine', this.coreDeps, this.makeDerivedDependencies(), this.extraModuleDependencies, options);
const moduleAPI = new ModuleAPI(mod, 'engine', this.coreDeps, this.makeDerivedDependencies(), this.extraModuleDependencies, options, this.hooks);
const moduleReturnValue = await cb(moduleAPI);

Object.assign(mod, moduleReturnValue);
Expand All @@ -186,12 +186,42 @@ export class Springboard {
};
};

private hookCallbacks = {
onUserConnect: [] as ((data: UserConnectData, users: UserConnectData[]) => void)[],
onUserDisconnect: [] as ((data: UserConnectData, users: UserConnectData[]) => void)[],
}

public hooks = {
onUserConnect: (callback: (data: UserConnectData, users: UserConnectData[]) => void) => {
this.hookCallbacks.onUserConnect.push(callback);
},
onUserDisconnect: (callback: (data: UserConnectData, users: UserConnectData[]) => void) => {
this.hookCallbacks.onUserDisconnect.push(callback);
},
// onReconnect: (callback: (data: UserConnectData) => void) => {
// this.hookCallbacks.onReconnect.push(callback);
// },
};

public hookTriggers = {
handleUserConnect: (data: UserConnectData, users: UserConnectData[]) => {
for (const callback of this.hookCallbacks.onUserConnect) {
callback(data, users);
}
},
handleUserDisconnect: (data: UserConnectData, users: UserConnectData[]) => {
for (const callback of this.hookCallbacks.onUserDisconnect) {
callback(data, users);
}
},
}

public registerClassModule = async <T extends object,>(cb: ClassModuleCallback<T>): Promise<Module | null> => {
const modDependencies = this.makeDerivedDependencies();

const mod = await Promise.resolve(cb(this.coreDeps, modDependencies));

const moduleAPI = new ModuleAPI(mod, 'engine', this.coreDeps, modDependencies, this.extraModuleDependencies, {});
const moduleAPI = new ModuleAPI(mod, 'engine', this.coreDeps, modDependencies, this.extraModuleDependencies, {}, this.hooks);

if (!isModuleEnabled(mod)) {
return null;
Expand Down Expand Up @@ -286,3 +316,7 @@ const Loader = () => {
</div>
);
};

type UserConnectData = {
id: string;
}
23 changes: 18 additions & 5 deletions packages/springboard/core/engine/module_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@ import {SharedStateSupervisor, StateSupervisor, UserAgentStateSupervisor} from '
import {ExtraModuleDependencies, Module, NavigationItemConfig, RegisteredRoute} from 'springboard/module_registry/module_registry';
import {CoreDependencies, ModuleDependencies} from '../types/module_types';
import {RegisterRouteOptions} from './register';
import {Springboard} from './engine';

type ActionConfigOptions = object;

export type ActionCallOptions = {
mode?: 'local' | 'remote';
}

export type UserData = {
userId: string;
}

/**
* The Action callback
*/
type ActionCallback<Args extends undefined | object, ReturnValue extends Promise<any> = Promise<any>> = (args: Args, options?: ActionCallOptions) => ReturnValue;
type ActionCallback<Args extends undefined | object, ReturnValue extends Promise<any> = Promise<any>> = (args: Args, options?: ActionCallOptions, userData?: UserData) => ReturnValue;

// this would make it so modules/plugins can extend the module API dynamically through interface merging
// export interface ModuleAPI {
Expand Down Expand Up @@ -51,7 +56,15 @@ export class ModuleAPI {

public readonly deps: {core: CoreDependencies; module: ModuleDependencies, extra: ExtraModuleDependencies};

constructor(private module: Module, private prefix: string, private coreDeps: CoreDependencies, private modDeps: ModuleDependencies, extraDeps: ExtraModuleDependencies, private options: ModuleOptions) {
constructor(
private module: Module,
private prefix: string,
private coreDeps: CoreDependencies,
private modDeps: ModuleDependencies,
extraDeps: ExtraModuleDependencies,
private options: ModuleOptions,
public hooks: Springboard['hooks'],
) {
this.deps = {core: coreDeps, module: modDeps, extra: extraDeps};
}

Expand Down Expand Up @@ -132,7 +145,7 @@ export class ModuleAPI {
actionName: string,
options: Options,
cb: undefined extends Args ? ActionCallback<Args, ReturnValue> : ActionCallback<Args, ReturnValue>
): undefined extends Args ? ((args?: Args, options?: ActionCallOptions) => ReturnValue) : ((args: Args, options?: ActionCallOptions) => ReturnValue) => {
): undefined extends Args ? ((args?: Args, options?: ActionCallOptions, userData?: UserData) => ReturnValue) : ((args: Args, options?: ActionCallOptions, userData?: UserData) => ReturnValue) => {
const fullActionName = `${this.fullPrefix}|action|${actionName}`;

if (this.coreDeps.rpc.remote.role === 'server') {
Expand All @@ -143,15 +156,15 @@ export class ModuleAPI {
this.coreDeps.rpc.local.registerRpc(fullActionName, cb);
}

return (async (args: Args, options?: ActionCallOptions): Promise<Awaited<ReturnValue>> => {
return (async (args: Args, options?: ActionCallOptions, userData?: UserData): Promise<Awaited<ReturnValue>> => {
try {
let rpc = this.coreDeps.rpc.remote;

// if (options?.mode === 'local' || rpc.role === 'server') { // TODO: get rid of isMaestro and do something like this instead

if (this.coreDeps.isMaestro() || this.options.rpcMode === 'local' || options?.mode === 'local') {
if (!this.coreDeps.rpc.local || this.coreDeps.rpc.local.role !== 'client') {
return await cb(args);
return await cb(args, options, userData);
}

rpc = this.coreDeps.rpc.local!;
Expand Down
Loading
Loading