diff --git a/API_ENDPOINTS.md b/API_ENDPOINTS.md new file mode 100644 index 00000000..07f0d728 --- /dev/null +++ b/API_ENDPOINTS.md @@ -0,0 +1,42 @@ +| Method | Endpoint | Class method | +|--------|------------------------------------------------|-----------------------------------------------------------| +| GET | /api/action/simpleList | ActionController.getList() | +| GET | /api/action/last_actions | ActionController.getLast20Events() | +| GET | /api/action/lastOverrides/:actionId | ActionController.getLastActionOverrides() | +| POST | /api/action/trigger/:actionId | ActionController.triggerAction() | +| GET | /api/actionActivity/current | ActionActivityController.getCurrentState() | +| GET | /api/config/ | ConfigController.getConfig() | +| PUT | /api/config/twitch | ConfigController.updateTwitchConfig() | +| PUT | /api/config/obs | ConfigController.updateObsConfig() | +| PUT | /api/config/ | ConfigController.updateConfig() | +| PUT | /api/config/customPort | ConfigController.updatePort() | +| DELETE | /api/config/twitchRevoke/:authType | ConfigController.revokeToken() | +| GET | /api/file/ | FileController.getList() | +| GET | /api/file/fileById/:mediaId | FileController.getById() | +| GET | /api/file/preview/:mediaId | FileController.getPreviewById() | +| GET | /api/screen/ | ScreenController.listAllScreens() | +| POST | /api/screen/ | ScreenController.addScreen() | +| PUT | /api/screen/:screenId | ScreenController.updateScreen() | +| DELETE | /api/screen/:screenId | ScreenController.deleteScreen() | +| PUT | /api/screen/:screenId/clips/bulk | ScreenController.updateScreenMediaBulk() | +| PUT | /api/screen/:screenId/clips/:mediaId | ScreenController.updateScreenMedia() | +| DELETE | /api/screen/:screenId/clips/:mediaId | ScreenController.deleteScreenMedia() | +| GET | /api/widget-state/:mediaId | WidgetStateController.getWidgetState() | +| PUT | /api/widget-state/:mediaId/:widgetInstance | WidgetStateController.updateScreen() | +| GET | /api/open/config | OpenController.openConfigPath() | +| GET | /api/open/files | OpenController.openFilePath() | +| GET | /api/twitchData/helix/* | TwitchDataController.getHelixData() | +| GET | /api/twitchData/authInformations | TwitchDataController.getTwitchAuthInformations() | +| GET | /api/twitchData/currentChannelPointRedemptions | TwitchDataController.listCurrentChannelPointRedemptions() | +| GET | /api/twitch_events/ | TwitchEventsController.getTwitchEvents() | +| POST | /api/twitch_events/ | TwitchEventsController.addTwitchEvent() | +| PUT | /api/twitch_events/:eventId | TwitchEventsController.updateTwitchEvent() | +| DELETE | /api/twitch_events/:eventId | TwitchEventsController.deleteTwitchEvent() | +| POST | /api/twitch_events/trigger_config_example | TwitchEventsController.triggerConfigExample() | +| POST | /api/twitch_events/trigger_event | TwitchEventsController.triggerEvent() | +| GET | /api/twitch_events/last_events | TwitchEventsController.getLast20Events() | +| GET | /api/obsData/currentBrowserSources | ObsDataController.listBrowserSources() | +| POST | /api/obsData/refreshBrowserSource/:sourceName | ObsDataController.refreshBrowserSource() | +| GET | /api/obsData/sceneList | ObsDataController.getSceneList() | +| GET | /api/obsData/sourceList | ObsDataController.getSourceList() | +| GET | /api/obsData/sourceFilters/:sourceName | ObsDataController.getSourceFilterList() | diff --git a/projects/contracts/src/lib/media.types.ts b/projects/contracts/src/lib/action.types.ts similarity index 100% rename from projects/contracts/src/lib/media.types.ts rename to projects/contracts/src/lib/action.types.ts diff --git a/projects/contracts/src/lib/types.ts b/projects/contracts/src/lib/types.ts index 4bb05c7b..04bfd485 100644 --- a/projects/contracts/src/lib/types.ts +++ b/projects/contracts/src/lib/types.ts @@ -1,5 +1,5 @@ import {ChatUserstate} from "tmi.js"; -import {ActionType} from "./media.types"; +import {ActionType} from "./action.types"; import {ActionOverridableProperties, TriggerAction} from "./actions"; import {AllTwitchEvents} from "./twitch.connector.types"; import {DefaultImage} from "./twitch-data.types"; diff --git a/projects/contracts/src/public-api.ts b/projects/contracts/src/public-api.ts index 7c4b1dd0..cb376524 100644 --- a/projects/contracts/src/public-api.ts +++ b/projects/contracts/src/public-api.ts @@ -4,7 +4,7 @@ export * from './lib/types'; export * from './lib/types.outdated'; -export * from './lib/media.types'; +export * from './lib/action.types'; export * from './lib/createInitialState'; export * from './lib/constants'; export * from './lib/actions'; diff --git a/projects/recipe-core/src/index.ts b/projects/recipe-core/src/index.ts index 159f6d39..a4f686a1 100644 --- a/projects/recipe-core/src/index.ts +++ b/projects/recipe-core/src/index.ts @@ -1,3 +1,6 @@ export * from './lib/recipe.types'; export * from './lib/utils'; export * from './lib/generateCodeByRecipe'; +export {RecipeCommandBlockGroups} from "./lib/recipeCommandBlockGroups"; +export {RecipeCommandRegistry} from "./lib/recipeCommandRegistry"; +export * from "./lib/recipeStepConfigArgument"; diff --git a/projects/recipe-core/src/lib/command-blocks.generic.ts b/projects/recipe-core/src/lib/command-blocks.generic.ts new file mode 100644 index 00000000..9e92c910 --- /dev/null +++ b/projects/recipe-core/src/lib/command-blocks.generic.ts @@ -0,0 +1,111 @@ +import {generateCodeByStep, RecipeCommandBlockRegistry} from "./recipe.types"; +import {generateRandomCharacters} from "./utils"; + +export function registerGenericCommandBlocks( + registry: RecipeCommandBlockRegistry, + generateCodeByStep: generateCodeByStep +): void { + registry["sleepSeconds"] = { + pickerLabel: "Wait for Seconds", + commandGroup: "generic", + configArguments: [ + { + name: "seconds", + label: "Seconds", + type: "number" + } + ], + toScriptCode: (step, context) => `sleep.secondsAsync(${step.payload.seconds});`, + commandEntryLabelAsync: (queries, payload, parentStep) => { + return Promise.resolve(`sleep: ${payload.seconds} seconds`); + }, + entryIcon: () => 'hourglass_top' + }; + + + registry["sleepMs"] = { + pickerLabel: "Wait for Milliseconds", + commandGroup: "generic", + configArguments: [ + { + name: "ms", + label: "Milliseconds", + type: "number" + } + ], + toScriptCode: (step, context) => `sleep.msAsync(${step.payload.ms});`, + commandEntryLabelAsync: (queries, payload, parentStep) => { + return Promise.resolve(`sleep: ${payload.ms}ms`); + }, + entryIcon: () => 'hourglass_top' + }; + + registry["randomCommandGroup"] = { + pickerLabel: "Random Command Group", + commandGroup: "generic", + configArguments: [ + { + name: 'amountOfGroups', + label: 'Amount of Groups', + type: "number" + } + ], + extendCommandBlockOnEdit: true, + extendCommandBlock: (step) => { + const amountOfGroups = step.payload.amountOfGroups as number; + + if (step.subCommandBlocks.length === amountOfGroups) { + return; + } + + if (amountOfGroups < step.subCommandBlocks.length) { + step.subCommandBlocks.length = amountOfGroups; + return; + } + + + const newSubCommandBlockArray = [...step.subCommandBlocks]; + + for (let i = step.subCommandBlocks.length; i < amountOfGroups; i++) { + newSubCommandBlockArray.push({ + labelId: generateRandomCharacters(5), + entries: [] + }); + } + + step.subCommandBlocks = newSubCommandBlockArray + }, + subCommandBlockLabelAsync: (queries, commandBlock, labelId) => { + return Promise.resolve(''); + }, + awaitCodeHandledInternally: true, + toScriptCode: (step, context, userData) => { + const awaitCode = step.awaited ? 'await ' : ''; + + const functionNames: string[] = []; + + const generatedFunctions = generateCodeByStep(step, context, userData).map(g => { + + const functionName = `randomGroup_${g.subCommand.labelId}`; + + functionNames.push(functionName); + + return `async function ${functionName}() { + ${g.generatedScript} + }`; + }).join('\r\n'); + + return ` + ${awaitCode} (() => { + ${generatedFunctions} + + const functionsToChoose = [${functionNames.join(',')}]; + + return utils.randomElement(functionsToChoose)(); + })();`; + }, + commandEntryLabelAsync: (queries, payload, parentStep) => { + return 'trigger any of these command groups randomly'; + } + }; +} diff --git a/projects/recipe-core/src/lib/command-blocks.memebox.ts b/projects/recipe-core/src/lib/command-blocks.memebox.ts index 0fdef817..270d2064 100644 --- a/projects/recipe-core/src/lib/command-blocks.memebox.ts +++ b/projects/recipe-core/src/lib/command-blocks.memebox.ts @@ -7,6 +7,7 @@ import { import {map, take} from "rxjs/operators"; import {generateRandomCharacters, listActionsOfActionListPayload} from "./utils"; import {ACTION_TYPE_INFORMATION} from "@memebox/contracts"; +import {AppQueries} from "@memebox/app-state"; function createMemeboxApiVariable( actionPayload: RecipeCommandConfigActionPayload @@ -27,10 +28,32 @@ function createMemeboxApiVariable( return actionApiVariable; } -export function registerMemeboxCommandBlocks ( +async function getActionLabel( + queries: AppQueries, + actionPayload: RecipeCommandConfigActionPayload, + prefix = '' +): Promise { + const actionName = await queries.getActionById$(actionPayload.actionId).pipe( + map(actionInfo => actionInfo?.name ?? 'unknown action'), + take(1) + ).toPromise(); + + const screenIdExist = !!actionPayload.screenId; + + const screenName = screenIdExist + ? await queries.screenMap$.pipe( + map(screenMap => screenMap[actionPayload.screenId!].name), + take(1) + ).toPromise() + : 'All Screens'; + + return `${prefix} [${actionName}] on [${screenName}]`.trim(); +} + +export function registerMemeboxCommandBlocks( registry: RecipeCommandBlockRegistry, generateCodeByStep: generateCodeByStep -): void { +): void { registry["triggerAction"] = { pickerLabel: "Trigger Action", commandGroup: "memebox", @@ -38,7 +61,10 @@ export function registerMemeboxCommandBlocks ( { name: "action", label: "Action to trigger", - type: "action" + type: "action", + flags: { + allowAllScreens: true + } } ], toScriptCode: (step) => { @@ -52,10 +78,7 @@ export function registerMemeboxCommandBlocks ( commandEntryLabelAsync: (queries, payload) => { const actionPayload = payload.action as RecipeCommandConfigActionPayload; - return queries.getActionById$(actionPayload.actionId).pipe( - map(actionInfo => actionInfo?.name ?? 'unknown action'), - take(1) - ).toPromise(); + return getActionLabel(queries, actionPayload); }, entryIcon: (queries, payload) => { const actionPayload = payload.action as RecipeCommandConfigActionPayload; @@ -64,7 +87,7 @@ export function registerMemeboxCommandBlocks ( map(action => ACTION_TYPE_INFORMATION[action.type].icon), take(1) ).toPromise(); - }, + } }; const keepTriggeredActionActiveWhileLabel = 'Keep Triggered Action active, while'; @@ -76,10 +99,13 @@ export function registerMemeboxCommandBlocks ( { name: "action", label: "Action to trigger, keep active", - type: "action" + type: "action", + flags: { + allowAllScreens: false + } } ], - subCommandBlockLabelAsync: (queries, commandBlock, labelId) => { + subCommandBlockLabelAsync: async (queries, commandBlock, labelId) => { const actionPayload = commandBlock.entryType === "command" && commandBlock.payload.action as RecipeCommandConfigActionPayload; @@ -87,10 +113,9 @@ export function registerMemeboxCommandBlocks ( return Promise.resolve('Unknown Action'); } - return queries.getActionById$(actionPayload.actionId).pipe( - map(action => `Keep Triggered Action ${action.name} active, while these commands are running: `), - take(1) - ).toPromise(); + const actionLabel = await getActionLabel(queries, actionPayload, 'Keep Triggered Action'); + + return actionLabel+' active, while these commands are running:'; }, extendCommandBlock: (step) => { step.payload = { @@ -98,7 +123,7 @@ export function registerMemeboxCommandBlocks ( _suffix: generateRandomCharacters(5) }; - step.subCommandBlocks.push( { + step.subCommandBlocks.push({ labelId: "keepVisibleWhile", entries: [] }); @@ -110,15 +135,20 @@ export function registerMemeboxCommandBlocks ( return `${createMemeboxApiVariable(actionPayload)} .triggerWhile(async (helpers_${step.payload._suffix}) => { - ${generateCodeByStep(step, context, userData)} + ${generateCodeByStep(step, context, userData)[0].generatedScript} } ${actionOverrides ? ',' + JSON.stringify(actionOverrides) : ''});`; }, commandEntryLabelAsync: (queries, payload) => { const actionPayload = payload.action as RecipeCommandConfigActionPayload; + return getActionLabel(queries, actionPayload); + }, + entryIcon: (queries, payload) => { + const actionPayload = payload.action as RecipeCommandConfigActionPayload; + return queries.getActionById$(actionPayload.actionId).pipe( - map(actionInfo => actionInfo?.name ?? 'unknown action'), + map(action => ACTION_TYPE_INFORMATION[action.type].icon), take(1) ).toPromise(); } @@ -142,7 +172,7 @@ export function registerMemeboxCommandBlocks ( }, commandEntryLabelAsync: () => { return Promise.resolve('reset'); - }, + } }; registry["triggerRandom"] = { @@ -157,7 +187,7 @@ export function registerMemeboxCommandBlocks ( ], awaitCodeHandledInternally: true, toScriptCode: (step, context, userData) => { - const awaitCode = step.awaited ? 'await ': ''; + const awaitCode = step.awaited ? 'await ' : ''; const actionsToChooseFrom = listActionsOfActionListPayload( step.payload.actions as RecipeCommandConfigActionListPayload, @@ -184,11 +214,11 @@ export function registerMemeboxCommandBlocks ( const tagName = tags.find(t => t.id === actionListPayload.actionsByTag)?.name ?? `\r\nUnknown Tag: ${actionListPayload.actionsByTag}`; - return `trigger any action with the tag: ${tagName}`; + return `trigger any action with the tag: [${tagName}]`; } return `trigger any of the following: ${actionListPayload.selectedActions.length}`; - }, + } }; @@ -199,8 +229,11 @@ export function registerMemeboxCommandBlocks ( { name: "action", label: "Action to update properties", - type: "action" - }, + type: "action", + flags: { + allowAllScreens: true + } + } /* { name: "overrides", label: "Properties to update", @@ -212,7 +245,7 @@ export function registerMemeboxCommandBlocks ( const actionPayload = step.payload.action as RecipeCommandConfigActionPayload; const overrides = actionPayload.overrides; - const awaitCode = step.awaited ? 'await ': ''; + const awaitCode = step.awaited ? 'await ' : ''; const updateVariables = overrides.action?.variables ? `promisesArray.push(action.updateVariables(${JSON.stringify(overrides.action)}));` @@ -239,10 +272,7 @@ ${awaitCode} (() => { commandEntryLabelAsync: (queries, payload) => { const actionPayload = payload.action as RecipeCommandConfigActionPayload; - return queries.getActionById$(actionPayload.actionId).pipe( - map(actionInfo => 'Update Properties of: ' + actionInfo?.name ?? 'unknown action'), - take(1) - ).toPromise(); + return getActionLabel(queries, actionPayload, 'Update Properties of:'); } }; } diff --git a/projects/recipe-core/src/lib/generateCodeByRecipe.ts b/projects/recipe-core/src/lib/generateCodeByRecipe.ts index 97c13e14..17f76443 100644 --- a/projects/recipe-core/src/lib/generateCodeByRecipe.ts +++ b/projects/recipe-core/src/lib/generateCodeByRecipe.ts @@ -1,6 +1,5 @@ import { - RecipeCommandBlockRegistry, - RecipeCommandSelectionGroup, + generatedCodeBySubCommandBlock, RecipeContext, RecipeEntry, RecipeEntryCommandCall, @@ -11,111 +10,58 @@ import {registerMemeboxCommandBlocks} from "./command-blocks.memebox"; import {registerObsCommandBlocks} from "./command-blocks.obs"; import {registerTwitchCommandBlocks} from "./command-blocks.twitch"; import {UserDataState} from "@memebox/contracts"; +import {RecipeCommandRegistry} from "./recipeCommandRegistry"; +import {registerGenericCommandBlocks} from "./command-blocks.generic"; -export interface RecipeStepConfigArgument { - name: string; - label: string; - type: string; // todo change to the enum -} - -export const RecipeCommandBlockGroups: Record = { - generic: { - label: "Generic", - order: 1 - }, - memebox: { - label: "Memebox", - order: 2 - }, - twitch: { - label: "Twitch", - order: 3 - }, - obs: { - label: "OBS", - order: 4 - } -}; - -export const RecipeCommandRegistry: RecipeCommandBlockRegistry = { - "sleepSeconds": { - pickerLabel: "Wait for Seconds", - commandGroup: "generic", - configArguments: [ - { - name: "seconds", - label: "Seconds", - type: "number" - } - ], - toScriptCode: (step, context) => `sleep.secondsAsync(${step.payload.seconds});`, - commandEntryLabelAsync: (queries, payload, parentStep) => { - return Promise.resolve(`sleep: ${payload.seconds} seconds`); - }, - entryIcon: () => 'hourglass_top' - }, - "sleepMs": { - pickerLabel: "Wait for Milliseconds", - commandGroup: "generic", - configArguments: [ - { - name: "ms", - label: "Milliseconds", - type: "number" - } - ], - toScriptCode: (step, context) => `sleep.msAsync(${step.payload.ms});`, - commandEntryLabelAsync: (queries, payload, parentStep) => { - return Promise.resolve(`sleep: ${payload.ms}ms`); - }, - entryIcon: () => 'hourglass_top' - } -}; - -function generateCodeByStepAsync (step: RecipeEntry, context: RecipeContext, userData: UserDataState): string { - const result: string[] = []; +function generateCodeByStepAsync (step: RecipeEntry, context: RecipeContext, userData: UserDataState): generatedCodeBySubCommandBlock[] { + const result: generatedCodeBySubCommandBlock[] = []; for (const subStepInfo of step.subCommandBlocks) { + const scriptCode: string[] = []; + for (const entryId of subStepInfo.entries) { const subEntry = context.entries[entryId]; if (!subEntry) { - result.push(`logger.error('this shouldnt have happened: cant find command block information of ${entryId});`); + scriptCode.push(`logger.error('this shouldnt have happened: cant find command block information of ${entryId});`); } else if (subEntry.entryType === 'command'){ const entryDefinition = RecipeCommandRegistry[subEntry.commandBlockType]; // result.push(`logger.log('Pre: ${subEntry.commandType}');`); if (!entryDefinition.awaitCodeHandledInternally && subEntry.awaited) { - result.push('await '); + scriptCode.push('await '); } const createdStepCode = entryDefinition.toScriptCode(subEntry, context, userData); - result.push(createdStepCode.trim()); + scriptCode.push(createdStepCode.trim()); // result.push(`logger.log('Post: ${subEntry.commandType}');`); } else { - result.push('TODO FOR TYPE: '+subEntry.entryType); + scriptCode.push('TODO FOR TYPE: '+subEntry.entryType); } } + + result.push({ + generatedScript: scriptCode.join('\r\n'), + subCommand: subStepInfo + }); } - return result.join('\r\n'); + return result; } export function generateCodeByRecipe( recipeContext: RecipeContext, userData: UserDataState ): string { - const result: string[] = []; - const rootEntry = recipeContext.entries[recipeContext.rootEntry]; - result.push(generateCodeByStepAsync(rootEntry, recipeContext, userData)); - - return result.join('\r\n'); + return generateCodeByStepAsync(rootEntry, recipeContext, userData) + .map(g => g.generatedScript) + .join('\r\n'); } export function generateRecipeEntryCommandCall ( @@ -132,6 +78,7 @@ export function generateRecipeEntryCommandCall ( }; } +registerGenericCommandBlocks(RecipeCommandRegistry, generateCodeByStepAsync); registerMemeboxCommandBlocks(RecipeCommandRegistry, generateCodeByStepAsync); registerObsCommandBlocks(RecipeCommandRegistry); registerTwitchCommandBlocks(RecipeCommandRegistry); diff --git a/projects/recipe-core/src/lib/recipe.types.ts b/projects/recipe-core/src/lib/recipe.types.ts index e53e38d6..273c1ee0 100644 --- a/projects/recipe-core/src/lib/recipe.types.ts +++ b/projects/recipe-core/src/lib/recipe.types.ts @@ -1,7 +1,7 @@ import {uuid} from "@gewd/utils"; import {ActionType, TriggerActionOverrides, UserDataState} from "@memebox/contracts"; import {AppQueries} from "@memebox/app-state"; -import {RecipeStepConfigArgument} from "./generateCodeByRecipe"; +import {RecipeStepConfigArguments} from "./recipeStepConfigArgument"; export interface RecipeSubCommandInfo { name: string; // property to save the subSteps @@ -18,13 +18,15 @@ export interface RecipeCommandInfo { // at some point custom controls/ui for each stepInfo could be shown +export interface RecipeSubCommandBlock { + labelId: string; + entries: string[] // entryId +} + export interface RecipeEntryBase { id: string; awaited?: boolean; - subCommandBlocks: { - labelId: string; - entries: string[] // entryId - }[]; + subCommandBlocks: RecipeSubCommandBlock[]; } export interface RecipeEntryCommandPayload { @@ -106,12 +108,13 @@ export interface RecipeCommandDefinition { subCommandBlockLabelAsync?: (queries: AppQueries, commandBlock: RecipeEntry, labelId: string) => Promise; entryIcon?: (queries: AppQueries, payload: RecipeEntryCommandPayload) => string|Promise; commandGroup: string; - configArguments: RecipeStepConfigArgument[]; // each argument name will be applied to the payload as prop + configArguments: RecipeStepConfigArguments[]; // each argument name will be applied to the payload as prop extendCommandBlock?: (step: RecipeEntryCommandCall, parentStep: RecipeEntry) => void; allowedToBeAdded?: (step: RecipeEntry, context: RecipeContext) => boolean; toScriptCode: (step: RecipeEntryCommandCall, context: RecipeContext, userData: UserDataState) => string; awaitCodeHandledInternally?: boolean; + extendCommandBlockOnEdit?: boolean; commandType?: string; } @@ -123,5 +126,8 @@ export interface RecipeCommandSelectionGroup { export interface RecipeCommandBlockRegistry { [stepType: string]: RecipeCommandDefinition } - -export type generateCodeByStep = (step: RecipeEntry, context: RecipeContext, userData: UserDataState) => string; +export interface generatedCodeBySubCommandBlock { + subCommand: RecipeSubCommandBlock; + generatedScript: string; +} +export type generateCodeByStep = (step: RecipeEntry, context: RecipeContext, userData: UserDataState) => generatedCodeBySubCommandBlock[]; diff --git a/projects/recipe-core/src/lib/recipeCommandBlockGroups.ts b/projects/recipe-core/src/lib/recipeCommandBlockGroups.ts new file mode 100644 index 00000000..c449308e --- /dev/null +++ b/projects/recipe-core/src/lib/recipeCommandBlockGroups.ts @@ -0,0 +1,20 @@ +import {RecipeCommandSelectionGroup} from "./recipe.types"; + +export const RecipeCommandBlockGroups: Record = { + generic: { + label: "Generic", + order: 1 + }, + memebox: { + label: "Memebox", + order: 2 + }, + twitch: { + label: "Twitch", + order: 3 + }, + obs: { + label: "OBS", + order: 4 + } +}; diff --git a/projects/recipe-core/src/lib/recipeCommandRegistry.ts b/projects/recipe-core/src/lib/recipeCommandRegistry.ts new file mode 100644 index 00000000..c858ae1e --- /dev/null +++ b/projects/recipe-core/src/lib/recipeCommandRegistry.ts @@ -0,0 +1,5 @@ +import {RecipeCommandBlockRegistry} from "./recipe.types"; + +export const RecipeCommandRegistry: RecipeCommandBlockRegistry = { + +}; diff --git a/projects/recipe-core/src/lib/recipeStepConfigArgument.ts b/projects/recipe-core/src/lib/recipeStepConfigArgument.ts new file mode 100644 index 00000000..01197eed --- /dev/null +++ b/projects/recipe-core/src/lib/recipeStepConfigArgument.ts @@ -0,0 +1,71 @@ +import {RecipeCommandConfigActionPayload} from "./recipe.types"; + +export interface RecipeStepConfigArgument { + name: string; + label: string; +} + +export interface RecipeStepConfigBooleanArgument extends RecipeStepConfigArgument { + type: 'boolean'; +} + +export interface RecipeStepConfigTextArgument extends RecipeStepConfigArgument { + type: 'text'; +} + +export interface RecipeStepConfigTextareaArgument extends RecipeStepConfigArgument { + type: 'textarea'; +} + +export interface RecipeStepConfigNumberArgument extends RecipeStepConfigArgument { + type: 'number'; +} + +export interface RecipeStepConfigActionArgument extends RecipeStepConfigArgument { + type: 'action'; + flags: { + allowAllScreens: boolean; + } +} + +export interface RecipeStepConfigActionListArgument extends RecipeStepConfigArgument { + type: 'actionList'; +} + +export interface RecipeStepConfigObsSceneArgument extends RecipeStepConfigArgument { + type: 'obs:scene'; +} +export interface RecipeStepConfigObsSourceArgument extends RecipeStepConfigArgument { + type: 'obs:source'; +} +export interface RecipeStepConfigObsFilterArgument extends RecipeStepConfigArgument { + type: 'obs:filter'; +} + + +export type RecipeStepConfigArguments = + RecipeStepConfigBooleanArgument |RecipeStepConfigTextArgument| + RecipeStepConfigTextareaArgument|RecipeStepConfigNumberArgument| + RecipeStepConfigActionArgument|RecipeStepConfigActionListArgument| +RecipeStepConfigObsSceneArgument|RecipeStepConfigObsSourceArgument| + RecipeStepConfigObsFilterArgument + ; + +// any since each paylaod has its own interface +export const RecipeStepConfigArgumentValidations: { + [typeOf: string]: (configArgument: any, currentConfigPayload: any) => boolean +} = { + ['action']: (configArgument: RecipeStepConfigActionArgument, currentConfigPayload: RecipeCommandConfigActionPayload) => { + const isUndefined = typeof currentConfigPayload.screenId === 'undefined' + + if (isUndefined){ + return false; + } + + if (configArgument.flags.allowAllScreens) { + return true; + } + + return !!currentConfigPayload.screenId; + } +} diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html index 30c0c5c6..6624c333 100644 --- a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html +++ b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.html @@ -50,8 +50,8 @@

Target Screen - - All + + All {{ screen.name }} diff --git a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.ts b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.ts index f5e59564..43a02189 100644 --- a/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.ts +++ b/projects/recipe-ui/src/lib/command-setting-dialog/step-setting-dialog.component.ts @@ -5,7 +5,8 @@ import { RecipeCommandConfigActionPayload, RecipeContext, RecipeEntryCommandPayload, - RecipeStepConfigArgument + RecipeStepConfigArguments, + RecipeStepConfigArgumentValidations } from "@memebox/recipe-core"; import { Action, @@ -16,12 +17,12 @@ import { } from "@memebox/contracts"; import {DialogService} from "../../../../../src/app/shared/dialogs/dialog.service"; import {Observable} from "rxjs"; -import {AppQueries} from "@memebox/app-state"; +import {AppQueries, SnackbarService} from "@memebox/app-state"; import cloneDeep from "lodash/cloneDeep"; import {isVisibleMedia} from "@memebox/shared-state"; export interface CommandSettingDialogPayload { - configArguments: RecipeStepConfigArgument[]; + configArguments: RecipeStepConfigArguments[]; currentStepData?: RecipeEntryCommandPayload; commandBlockName: string; recipeContext: RecipeContext; @@ -44,6 +45,7 @@ export class StepSettingDialogComponent { private dialogRef: MatDialogRef, private dialogService: DialogService, private appQuery: AppQueries, + private snackbarService: SnackbarService ) { this._prepareCurrentPayload(); } @@ -107,6 +109,18 @@ export class StepSettingDialogComponent { } save(): void { + for (const configArgument of this.data.configArguments) { + if (RecipeStepConfigArgumentValidations[configArgument.type]){ + if (!RecipeStepConfigArgumentValidations[configArgument.type]( + configArgument, this.payload[configArgument.name] + )){ + this.snackbarService.sorry(`"${configArgument.label}" is invalid`); + // todo mark those inputs as invalid with a hint or something + return; + } + } + } + this.dialogRef.close(this.payload); } diff --git a/projects/recipe-ui/src/lib/recipe-block/recipe-block.component.html b/projects/recipe-ui/src/lib/recipe-block/recipe-block.component.html index 632c988b..32216c63 100644 --- a/projects/recipe-ui/src/lib/recipe-block/recipe-block.component.html +++ b/projects/recipe-ui/src/lib/recipe-block/recipe-block.component.html @@ -38,7 +38,6 @@ -
diff --git a/projects/recipe-ui/src/lib/recipe-block/recipe-block.component.ts b/projects/recipe-ui/src/lib/recipe-block/recipe-block.component.ts index c8b9941b..83436703 100644 --- a/projects/recipe-ui/src/lib/recipe-block/recipe-block.component.ts +++ b/projects/recipe-ui/src/lib/recipe-block/recipe-block.component.ts @@ -1,5 +1,5 @@ import {ChangeDetectorRef, Component, HostBinding, Input, OnDestroy, OnInit} from '@angular/core'; -import {RecipeEntry, RecipeEntryCommandCall, RecipeSubCommandInfo} from "@memebox/recipe-core"; +import {RecipeCommandRegistry, RecipeEntry, RecipeEntryCommandCall, RecipeSubCommandInfo} from "@memebox/recipe-core"; import {CdkDragDrop} from "@angular/cdk/drag-drop"; import {RecipeContextDirective} from "../recipe-context.directive"; import {DialogService} from "../../../../../src/app/shared/dialogs/dialog.service"; @@ -105,7 +105,22 @@ export class RecipeBlockComponent const newPayload = await this.stepCreator.editStepData(entry, this.context.recipe!); if (newPayload) { - this.context.changePayload(entry, newPayload); + if (RecipeCommandRegistry[entry.commandBlockType].extendCommandBlockOnEdit) { + const newEntry = { + ...entry, + payload:newPayload, + subCommandBlocks: [...entry.subCommandBlocks] + }; + + RecipeCommandRegistry[entry.commandBlockType]?.extendCommandBlock?.(newEntry, parent) + + this.context.changeEntry(newEntry); + } + else { + + this.context.changePayload(entry, newPayload); + } + } } } diff --git a/projects/recipe-ui/src/lib/recipe-context.directive.ts b/projects/recipe-ui/src/lib/recipe-context.directive.ts index 5d91ad21..4e91fee9 100644 --- a/projects/recipe-ui/src/lib/recipe-context.directive.ts +++ b/projects/recipe-ui/src/lib/recipe-context.directive.ts @@ -164,4 +164,10 @@ export class RecipeContextDirective } }); } + + changeEntry(entry: RecipeEntryCommandCall): void{ + this.update(state => { + state.entries[entry.id] = entry; + }) +} } diff --git a/server/logger.utils.ts b/server/logger.utils.ts index 1c396f19..e9eefcd1 100644 --- a/server/logger.utils.ts +++ b/server/logger.utils.ts @@ -13,7 +13,7 @@ export function newLogger(label: string) { export const LOGGER = newLogger('MemeBox'); -LOGGER.info('########## Started ##########'); +LOGGER.info('########## Started Log ##########'); function logAndExit (type: string) { process.on(type as any, (err: Error) => { diff --git a/server/persistence.functions.ts b/server/persistence.functions.ts index ebbbf81f..a6e41c11 100644 --- a/server/persistence.functions.ts +++ b/server/persistence.functions.ts @@ -1,7 +1,6 @@ -import { createDirIfNotExists, MEDIA_SCREENSHOT_PATH, safeResolve } from "./path.utils"; -import { Action } from "../projects/contracts/src/lib/types"; -import { ActionType } from "../projects/contracts/src/lib/media.types"; -import fs, { writeFileSync } from "fs"; +import {createDirIfNotExists, MEDIA_SCREENSHOT_PATH, safeResolve} from "./path.utils"; +import {Action, ActionType} from "@memebox/contracts"; +import fs, {writeFileSync} from "fs"; import path from "path"; export const GetPreviewFilePath = (actionId: string) => safeResolve(MEDIA_SCREENSHOT_PATH, actionId+'.jpg'); diff --git a/server/persistence.ts b/server/persistence.ts index a020156b..179b5556 100644 --- a/server/persistence.ts +++ b/server/persistence.ts @@ -612,7 +612,14 @@ export const PersistenceInstance = new Persistence( ); // todo refactor it to a new place when the new logger is being used -LOGGER.info({CLI_OPTIONS, LOG_PATH, NEW_CONFIG_PATH}); +LOGGER.info('Config Path:'+ NEW_CONFIG_PATH); +LOGGER.info('Log Path:'+ LOG_PATH); + +Object.entries(CLI_OPTIONS).forEach(([optionKey, optionValue]) => { + if (optionValue){ + LOGGER.info(optionKey,': ',optionValue); + } +}) PERSISTENCE.instance = PersistenceInstance; diff --git a/server/providers/actions/action-trigger.handler.ts b/server/providers/actions/action-trigger.handler.ts index 280f41fa..07372d5e 100644 --- a/server/providers/actions/action-trigger.handler.ts +++ b/server/providers/actions/action-trigger.handler.ts @@ -1,5 +1,15 @@ import {Service, UseOpts} from "@tsed/di"; -import {Action, ACTIONS, ActionStateEnum, ActionType, Dictionary, Screen, TriggerAction} from "@memebox/contracts"; +import { + Action, + ACTION_TYPE_INFORMATION, + ACTIONS, + ActionStateEnum, + ActionType, + Dictionary, + Screen, + TriggerAction, + TriggerActionOrigin +} from "@memebox/contracts"; import {Persistence} from "../../persistence"; import {NamedLogger} from "../named-logger"; import {Inject} from "@tsed/common"; @@ -15,8 +25,7 @@ import {ActionActiveState} from "./action-active-state"; @Service() export class ActionTriggerHandler { private _allScreens: Screen[] = []; - private _allMediasMap: Dictionary = {}; - private _allMediasList: Action[] = []; + private _allActionsMap: Dictionary = {}; private _actionQueue = new ActionQueue( this._persistence, @@ -50,30 +59,30 @@ export class ActionTriggerHandler { this.getData(); } - async triggerActionById(payloadObs: TriggerAction) { - this.logger.info(`Clip triggered: ${payloadObs.id} - Target: ${payloadObs.targetScreen ?? 'Any'} - Origin: ${payloadObs.origin}`, payloadObs); + async triggerActionById(triggerPayload: TriggerAction) { + this.logTriggerInformation(triggerPayload); - const mediaConfig = this._allMediasMap[payloadObs.id]; + const mediaConfig = this._allActionsMap[triggerPayload.id]; switch (mediaConfig.type) { case ActionType.Script: - await this._scriptHandler.handleScript(mediaConfig, payloadObs); + await this._scriptHandler.handleScript(mediaConfig, triggerPayload); break; case ActionType.Recipe: - await this._scriptHandler.handleRecipe(mediaConfig, payloadObs); + await this._scriptHandler.handleRecipe(mediaConfig, triggerPayload); break; default: { - if (payloadObs.targetScreen) { - this.triggerActionOnScreen(payloadObs); + if (triggerPayload.targetScreen) { + this.triggerActionOnScreen(triggerPayload); return; } // No Meta Type // Trigger the action on all assign screens for (const screen of this._allScreens) { - if (screen.clips[payloadObs.id]) { + if (screen.clips[triggerPayload.id]) { const newMessageObj: TriggerAction = { - ...payloadObs, + ...triggerPayload, targetScreen: screen.id }; @@ -83,7 +92,7 @@ export class ActionTriggerHandler { } } - return payloadObs; + return triggerPayload; } triggerActionOnScreen (payload: TriggerAction): void { @@ -121,9 +130,24 @@ export class ActionTriggerHandler { } } + private logTriggerInformation(triggerPayload: TriggerAction) { + const actionInfo = this._allActionsMap[triggerPayload.id]; + + const screenName = triggerPayload.targetScreen + ? this._allScreens.find(s => s.id === triggerPayload.targetScreen)?.name ?? '[Unknown] '+triggerPayload.targetScreen + : 'any'; + + const originLabel = triggerPayload.origin + ? TriggerActionOrigin[triggerPayload.origin] + : '[Unknown] ' +triggerPayload.origin; + + const typeName = ACTION_TYPE_INFORMATION[actionInfo.type]?.labelFallback ?? '[Unknown]'; + + this.logger.info(`${typeName} triggered: "${actionInfo.name}" - Target-Screen: ${screenName} - Origin: ${originLabel}`, triggerPayload); + } + private getData() { this._allScreens = this._persistence.listScreens(); - this._allMediasMap = this._persistence.fullState().clips; - this._allMediasList = Object.values(this._allMediasMap); + this._allActionsMap = this._persistence.fullState().clips; } } diff --git a/server/providers/actions/scripts/script.handler.ts b/server/providers/actions/scripts/script.handler.ts index 6ca02066..a5603e8d 100644 --- a/server/providers/actions/scripts/script.handler.ts +++ b/server/providers/actions/scripts/script.handler.ts @@ -1,6 +1,13 @@ import {Service, UseOpts} from "@tsed/di"; import {VM} from "vm2"; -import {Action, ActionStateEnum, ActionType, getUserDataState, TriggerAction} from "@memebox/contracts"; +import { + Action, + ACTION_TYPE_INFORMATION, + ActionStateEnum, + ActionType, + getUserDataState, + TriggerAction +} from "@memebox/contracts"; import {NamedLogger} from "../../named-logger"; import {Inject} from "@tsed/common"; import {PERSISTENCE_DI} from "../../contracts"; @@ -116,8 +123,6 @@ export class ScriptHandler implements ActionStoreAdapter { } public handleScript (script: Action, payloadObs: TriggerAction) { - this.logger.info('Handle Script!!'); - const scriptConfig = actionDataToScriptConfig(script); return this.handleGenericScript(script, scriptConfig, payloadObs); @@ -128,7 +133,7 @@ export class ScriptHandler implements ActionStoreAdapter { scriptConfig: ScriptConfig, payloadObs: TriggerAction ) { - // todo rename logs per target type script/recipe + this.logScript(script,`is starting.`); this.actionStateEventBus.updateActionState({ mediaId: script.id, @@ -158,7 +163,7 @@ export class ScriptHandler implements ActionStoreAdapter { try { scriptHoldingData.compile(); } catch (err) { - this.logger.error(err.message, `Script: ${script.name}`); + this.logScript(script,``, err); return; } this._compiledScripts.set(script.id, scriptHoldingData); @@ -167,14 +172,14 @@ export class ScriptHandler implements ActionStoreAdapter { try { await scriptHoldingData.execute(payloadObs); } catch (err) { - this.logger.error(err, `Failed to run script for "${script.name}" [${script.id}]`); + this.logScript(script, `Failed to run [${script.id}]`, err); } const scriptRunningType = script.type === ActionType.PermanentScript ? 'is running' : 'is done'; - this.logger.info(`Script "${script.name}" ${scriptRunningType}.`); + this.logScript(script,`${scriptRunningType}.`); this.actionStateEventBus.updateActionState({ mediaId: script.id, @@ -213,11 +218,21 @@ export class ScriptHandler implements ActionStoreAdapter { cachedCompiledScript.dispose(); if (cachedCompiledScript.script.type === ActionType.PermanentScript) { - this.logger.info(`Script: "${cachedCompiledScript.script.name}" stopped.`); + this.logScript(cachedCompiledScript.script,`stopped.`); } else { - this.logger.info(`Script Cache: "${cachedCompiledScript.script.name}" cleared.`); + this.logScript(cachedCompiledScript.script,`Cache cleared.`); } this._compiledScripts.delete(scriptId); } + + private logScript(action: Action, logMessage: string, error?: unknown){ + const scriptType = ACTION_TYPE_INFORMATION[action.type]?.labelFallback ?? '[Unknown]'; + + if (error){ + this.logger.error(error, `${scriptType} "${action.name}": ${logMessage}`) + } + this.logger.info(`${scriptType} "${action.name}": ${logMessage}`) + } + } diff --git a/server/server-app.ts b/server/server-app.ts index 49cde792..7505da12 100644 --- a/server/server-app.ts +++ b/server/server-app.ts @@ -17,6 +17,8 @@ import {CLI_OPTIONS} from "./utils/cli-options"; // TODO use config values? +LOGGER.info('Version: '+currentVersionJson.VERSION_TAG, '-', currentVersionJson.COMMIT); + const CONFIG_IS_LOADED$ = PersistenceInstance.configLoaded$.pipe( take(1) ).toPromise(); diff --git a/src/app/shared/dialogs/screen-clip-options/screen-clip-options.component.html b/src/app/shared/dialogs/screen-clip-options/screen-clip-options.component.html index c09d38c3..1c386f9e 100644 --- a/src/app/shared/dialogs/screen-clip-options/screen-clip-options.component.html +++ b/src/app/shared/dialogs/screen-clip-options/screen-clip-options.component.html @@ -94,7 +94,9 @@

+ type="number" + placeholder="777" + > ms @@ -123,7 +125,9 @@

+ type="number" + placeholder="777" + > ms diff --git a/src/version_info.json b/src/version_info.json index 5b2533e9..1d1e0810 100644 --- a/src/version_info.json +++ b/src/version_info.json @@ -1,6 +1,6 @@ { "BUILD_TIME": "Some time", - "COMMIT": "commit dummy", + "COMMIT": "1337C0DE", "BRANCH": "develop", "VERSION_TAG": "0.0.0" }