From 32dd090a66c8d6070818d9fbc16891ad8b67858c Mon Sep 17 00:00:00 2001 From: Sean Beyer Date: Tue, 10 Sep 2024 13:49:01 -0700 Subject: [PATCH] Add option to include filter on overlay and widget installs --- app/components-react/pages/BrowseOverlays.tsx | 57 ++++++++++++++----- app/services/source-filters.ts | 37 +++++++++++- app/services/widgets/widgets.ts | 13 ++++- 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/app/components-react/pages/BrowseOverlays.tsx b/app/components-react/pages/BrowseOverlays.tsx index 6c70718d61dd..2e86f1fd71ac 100644 --- a/app/components-react/pages/BrowseOverlays.tsx +++ b/app/components-react/pages/BrowseOverlays.tsx @@ -9,6 +9,12 @@ import { GuestApiHandler } from 'util/guest-api-handler'; import { IDownloadProgress } from 'util/requests'; import * as remote from '@electron/remote'; import { Services } from 'components-react/service-provider'; +import { IOverlayFilter } from 'services/source-filters'; + +interface IOverlayOptions { + mergePlatform?: boolean; + filter?: IOverlayFilter; +} export default function BrowseOverlays(p: { params: { type?: 'overlay' | 'widget-themes' | 'site-themes'; id?: string; install?: string }; @@ -24,13 +30,18 @@ export default function BrowseOverlays(p: { NotificationsService, JsonrpcService, RestreamService, + SourceFiltersService, } = Services; const [downloading, setDownloading] = useState(false); const [overlaysUrl, setOverlaysUrl] = useState(''); useEffect(() => { async function getOverlaysUrl() { - const url = await UserService.actions.return.overlaysUrl(p.params?.type, p.params?.id, p.params?.install); + const url = await UserService.actions.return.overlaysUrl( + p.params?.type, + p.params?.id, + p.params?.install, + ); if (!url) return; setOverlaysUrl(url); } @@ -70,13 +81,14 @@ export default function BrowseOverlays(p: { url: string, name: string, progressCallback?: (progress: IDownloadProgress) => void, - mergePlatform = false, + opts: IOverlayOptions = { mergePlatform: false }, ) { try { - await installOverlayBase(url, name, progressCallback, mergePlatform); + await installOverlayBase(url, name, progressCallback, opts); NavigationService.actions.navigate('Studio'); - } catch(e) { + } catch (e: unknown) { // If the overlay requires platform merge, navigate to the platform merge page + // @ts-ignore we know this type of error if (e.message === 'REQUIRES_PLATFORM_MERGE') { NavigationService.actions.navigate('PlatformMerge', { overlayUrl: url, overlayName: name }); } else { @@ -89,7 +101,7 @@ export default function BrowseOverlays(p: { url: string, name: string, progressCallback?: (progress: IDownloadProgress) => void, - mergePlatform = false + opts: IOverlayOptions = { mergePlatform: false }, ) { return new Promise((resolve, reject) => { const host = new urlLib.URL(url).hostname; @@ -106,7 +118,7 @@ export default function BrowseOverlays(p: { // User should be eligible to enable restream for this behavior to work. // If restream is already set up, then just install as normal. if ( - mergePlatform && + opts.mergePlatform && UserService.state.auth?.platforms.facebook && RestreamService.views.canEnableRestream && !RestreamService.shouldGoLiveWithRestream @@ -115,12 +127,18 @@ export default function BrowseOverlays(p: { } else { setDownloading(true); const sub = SceneCollectionsService.downloadProgress.subscribe(progressCallback); - SceneCollectionsService.actions.return.installOverlay(url, name) + SceneCollectionsService.actions.return + .installOverlay(url, name) .then(() => { sub.unsubscribe(); setDownloading(false); resolve(); }) + .then(() => { + if (opts.filter) { + SourceFiltersService.actions.applyFilterToOverlay(opts.filter); + } + }) .catch((e: unknown) => { sub.unsubscribe(); setDownloading(false); @@ -130,8 +148,8 @@ export default function BrowseOverlays(p: { }); } - async function installWidgets(urls: string[]) { - await installWidgetsBase(urls); + async function installWidgets(urls: string[], opts?: IOverlayOptions) { + await installWidgetsBase(urls, opts); NavigationService.actions.navigate('Studio'); NotificationsService.actions.push({ @@ -146,7 +164,7 @@ export default function BrowseOverlays(p: { }); } - async function installWidgetsBase(urls: string[]) { + async function installWidgetsBase(urls: string[], opts?: IOverlayOptions) { for (const url of urls) { const host = new urlLib.URL(url).hostname; const trustedHosts = ['cdn.streamlabs.com']; @@ -157,16 +175,25 @@ export default function BrowseOverlays(p: { } const path = await OverlaysPersistenceService.actions.return.downloadOverlay(url); - await WidgetsService.actions.return.loadWidgetFile(path, ScenesService.views.activeSceneId); + await WidgetsService.actions.return.loadWidgetFile( + path, + ScenesService.views.activeSceneId, + opts?.filter, + ); } } - async function installOverlayAndWidgets(overlayUrl: string, overlayName: string, widgetUrls: string[]) { + async function installOverlayAndWidgets( + overlayUrl: string, + overlayName: string, + widgetUrls: string[], + opts: IOverlayOptions = { mergePlatform: false }, + ) { try { - await installOverlayBase(overlayUrl, overlayName); - await installWidgetsBase(widgetUrls); + await installOverlayBase(overlayUrl, overlayName, () => {}, opts); + await installWidgetsBase(widgetUrls, opts); NavigationService.actions.navigate('Studio'); - } catch (e) { + } catch (e: unknown) { console.error(e); } } diff --git a/app/services/source-filters.ts b/app/services/source-filters.ts index 30112a73f856..95fd88d6789e 100644 --- a/app/services/source-filters.ts +++ b/app/services/source-filters.ts @@ -10,7 +10,7 @@ import { import { metadata } from 'components/shared/inputs'; import path from 'path'; import { Inject } from './core/injector'; -import { SourcesService } from './sources'; +import { SourcesService, TSourceType } from './sources'; import { WindowsService } from './windows'; import * as obs from '../../obs-api'; import namingHelpers from '../util/NamingHelpers'; @@ -61,6 +61,11 @@ interface ISourceFilterType { async: boolean; } +export interface IOverlayFilter { + type: TSourceFilterType; + settings: Dictionary; +} + export interface ISourceFilter { name: string; type: TSourceFilterType; @@ -271,7 +276,6 @@ export class SourceFiltersService extends StatefulService const obsSource = source.getObsInput(); obsSource.addFilter(obsFilter); - // The filter should be created with the settings provided, is this necessary? if (settings) obsFilter.update(settings); const filterReference = obsSource.findFilter(filterName); // There is now 2 references to the filter at that point @@ -454,6 +458,35 @@ export class SourceFiltersService extends StatefulService return this.sourcesService.views.getSource(sourceId).getObsInput().findFilter(filterName); } + sourceCanHaveOverlayFilters(sourceType: TSourceType) { + return [ + 'image_source', + 'color_source', + 'ffmpeg_source', + 'text_gdiplus', + 'text_ft2_source', + 'browser_source', + 'slideshow', + ].includes(sourceType); + } + + applyFilterToOverlay(filter: IOverlayFilter) { + const sources = this.sourcesService.views.getSources(); + sources.forEach(source => { + if (!this.sourceCanHaveOverlayFilters(source.type)) return; + this.addOverlayFilter(source.sourceId, filter); + }); + } + + addOverlayFilter(sourceId: string, filter: IOverlayFilter) { + const filterTypes = this.views.getTypesForSource(sourceId); + const applicableFilter = filterTypes.find(filterType => filterType.type === filter.type); + if (applicableFilter) { + const name = this.views.suggestName(sourceId, applicableFilter.description); + this.add(sourceId, filter.type, name, filter.settings); + } + } + @mutation() SET_FILTERS(sourceId: string, filters: ISourceFilter[]) { Vue.set(this.state.filters, sourceId, [...filters]); diff --git a/app/services/widgets/widgets.ts b/app/services/widgets/widgets.ts index 3d6117ff011b..7c3ceaa0b408 100644 --- a/app/services/widgets/widgets.ts +++ b/app/services/widgets/widgets.ts @@ -26,6 +26,7 @@ import { getWidgetsConfig } from './widgets-config'; import { WidgetDisplayData } from '.'; import { DualOutputService } from 'services/dual-output'; import { TDisplayType, VideoSettingsService } from 'services/settings-v2'; +import { IOverlayFilter, SourceFiltersService } from 'services/source-filters'; export interface IWidgetSourcesState { widgetSources: Dictionary; @@ -83,6 +84,7 @@ export class WidgetsService @Inject() editorCommandsService: EditorCommandsService; @Inject() dualOutputService: DualOutputService; @Inject() videoSettingsService: VideoSettingsService; + @Inject() sourceFiltersService: SourceFiltersService; widgetDisplayData = WidgetDisplayData(); // cache widget display data @@ -353,8 +355,9 @@ export class WidgetsService * Load a widget file from the given path * @param path the path to the widget file to laod * @param sceneId the id of the scene to load into + * @param filter optional filter attached to a library widget (ie hue adjust) */ - async loadWidgetFile(path: string, sceneId: string) { + async loadWidgetFile(path: string, sceneId: string, filter?: IOverlayFilter) { const scene = this.scenesService.views.getScene(sceneId); const json = await new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { @@ -367,7 +370,7 @@ export class WidgetsService }); const widget = JSON.parse(json); - this.importWidgetJSON(widget, scene); + this.importWidgetJSON(widget, scene, filter); } /** @@ -375,7 +378,7 @@ export class WidgetsService * @param widget the widget to import * @param scene the scene to import into */ - private importWidgetJSON(widget: ISerializableWidget, scene: Scene) { + private importWidgetJSON(widget: ISerializableWidget, scene: Scene, filter?: IOverlayFilter) { let widgetItem: SceneItem; // First, look for an existing widget of the same type @@ -401,6 +404,10 @@ export class WidgetsService 'horizontal', ); + if (filter) { + this.sourceFiltersService.actions.addOverlayFilter(widgetItem.sourceId, filter); + } + // if this is a dual output scene, also create the vertical scene item if (this.dualOutputService.views.hasNodeMap()) { Promise.resolve(